newAuthorizationHeaders = Collections.singletonList(JWT_ACCESS_PREFIX + jwt);
+ return Collections.singletonMap(AuthHttpConstants.AUTHORIZATION, newAuthorizationHeaders);
+ }
+ }
+
+ @Override
+ public boolean hasRequestMetadata() {
+ return true;
+ }
+
+ @Override
+ public boolean hasRequestMetadataOnly() {
+ return true;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof JwtCredentials)) {
+ return false;
+ }
+ JwtCredentials other = (JwtCredentials) obj;
+ return Objects.equals(this.privateKey, other.privateKey)
+ && Objects.equals(this.privateKeyId, other.privateKeyId)
+ && Objects.equals(this.jwtClaims, other.jwtClaims)
+ && Objects.equals(this.lifeSpanSeconds, other.lifeSpanSeconds);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.privateKey, this.privateKeyId, this.jwtClaims, this.lifeSpanSeconds);
+ }
+
+ Clock getClock() {
+ if (clock == null) {
+ clock = Clock.SYSTEM;
+ }
+ return clock;
+ }
+
+ public static class Builder {
+ private PrivateKey privateKey;
+ private String privateKeyId;
+ private JwtClaims jwtClaims;
+ private Clock clock = Clock.SYSTEM;
+ private Long lifeSpanSeconds = TimeUnit.HOURS.toSeconds(1);
+
+ protected Builder() {}
+
+ public Builder setPrivateKey(PrivateKey privateKey) {
+ this.privateKey = Preconditions.checkNotNull(privateKey);
+ return this;
+ }
+
+ public PrivateKey getPrivateKey() {
+ return privateKey;
+ }
+
+ public Builder setPrivateKeyId(String privateKeyId) {
+ this.privateKeyId = Preconditions.checkNotNull(privateKeyId);
+ return this;
+ }
+
+ public String getPrivateKeyId() {
+ return privateKeyId;
+ }
+
+ public Builder setJwtClaims(JwtClaims claims) {
+ this.jwtClaims = Preconditions.checkNotNull(claims);
+ return this;
+ }
+
+ public JwtClaims getJwtClaims() {
+ return jwtClaims;
+ }
+
+ public Builder setLifeSpanSeconds(Long lifeSpanSeconds) {
+ this.lifeSpanSeconds = Preconditions.checkNotNull(lifeSpanSeconds);
+ return this;
+ }
+
+ public Long getLifeSpanSeconds() {
+ return lifeSpanSeconds;
+ }
+
+ Builder setClock(Clock clock) {
+ this.clock = Preconditions.checkNotNull(clock);
+ return this;
+ }
+
+ Clock getClock() {
+ return clock;
+ }
+
+ public JwtCredentials build() {
+ return new JwtCredentials(this);
+ }
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/oauth2/JwtProvider.java b/oauth2_http/java/com/google/auth/oauth2/JwtProvider.java
new file mode 100644
index 000000000..eadee9600
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/JwtProvider.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2019, Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import com.google.common.annotations.Beta;
+
+/** Interface for creating custom JWT tokens */
+@Beta
+public interface JwtProvider {
+
+ /**
+ * Returns a new JwtCredentials instance with modified claims.
+ *
+ * @param newClaims new claims. Any unspecified claim fields will default to the the current
+ * values.
+ * @return new credentials
+ */
+ JwtCredentials jwtWithClaims(JwtClaims newClaims);
+}
diff --git a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java
index da89f9685..e70af3a70 100644
--- a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java
@@ -83,7 +83,8 @@
*
* By default uses a JSON Web Token (JWT) to fetch access tokens.
*/
-public class ServiceAccountCredentials extends GoogleCredentials implements ServiceAccountSigner {
+public class ServiceAccountCredentials extends GoogleCredentials
+ implements JwtProvider, ServiceAccountSigner {
private static final long serialVersionUID = 7807543542681217978L;
private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
@@ -525,6 +526,25 @@ public byte[] sign(byte[] toSign) {
}
}
+ /**
+ * Returns a new JwtCredentials instance with modified claims.
+ *
+ * @param newClaims new claims. Any unspecified claim fields will default to the the current
+ * values.
+ * @return new credentials
+ */
+ @Override
+ public JwtCredentials jwtWithClaims(JwtClaims newClaims) {
+ JwtClaims.Builder claimsBuilder =
+ JwtClaims.newBuilder().setIssuer(clientEmail).setSubject(clientEmail);
+ return JwtCredentials.newBuilder()
+ .setPrivateKey(privateKey)
+ .setPrivateKeyId(privateKeyId)
+ .setJwtClaims(claimsBuilder.build().merge(newClaims))
+ .setClock(clock)
+ .build();
+ }
+
@Override
public int hashCode() {
return Objects.hash(
diff --git a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountJwtAccessCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountJwtAccessCredentials.java
index 16b61c6d4..12aa73775 100644
--- a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountJwtAccessCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountJwtAccessCredentials.java
@@ -36,14 +36,11 @@
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.JsonObjectParser;
-import com.google.api.client.json.webtoken.JsonWebSignature;
-import com.google.api.client.json.webtoken.JsonWebToken;
import com.google.api.client.util.Clock;
import com.google.api.client.util.Preconditions;
import com.google.auth.Credentials;
import com.google.auth.RequestMetadataCallback;
import com.google.auth.ServiceAccountSigner;
-import com.google.auth.http.AuthHttpConstants;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Throwables;
@@ -56,13 +53,11 @@
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.net.URI;
-import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
-import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -76,18 +71,21 @@
*
Uses a JSON Web Token (JWT) directly in the request metadata to provide authorization.
*/
public class ServiceAccountJwtAccessCredentials extends Credentials
- implements ServiceAccountSigner {
+ implements JwtProvider, ServiceAccountSigner {
private static final long serialVersionUID = -7274955171379494197L;
static final String JWT_ACCESS_PREFIX = OAuth2Utils.BEARER_PREFIX;
+
@VisibleForTesting static final long LIFE_SPAN_SECS = TimeUnit.HOURS.toSeconds(1);
+ private static final long CLOCK_SKEW = TimeUnit.MINUTES.toSeconds(5);
private final String clientId;
private final String clientEmail;
private final PrivateKey privateKey;
private final String privateKeyId;
private final URI defaultAudience;
- private transient LoadingCache tokenCache;
+
+ private transient LoadingCache credentialsCache;
// Until we expose this to the users it can remain transient and non-serializable
@VisibleForTesting transient Clock clock = Clock.SYSTEM;
@@ -128,7 +126,7 @@ private ServiceAccountJwtAccessCredentials(
this.privateKey = Preconditions.checkNotNull(privateKey);
this.privateKeyId = privateKeyId;
this.defaultAudience = defaultAudience;
- this.tokenCache = createCache();
+ this.credentialsCache = createCache();
}
/**
@@ -253,10 +251,10 @@ public static ServiceAccountJwtAccessCredentials fromStream(
fileType, SERVICE_ACCOUNT_FILE_TYPE));
}
- private LoadingCache createCache() {
+ private LoadingCache createCache() {
return CacheBuilder.newBuilder()
.maximumSize(100)
- .expireAfterWrite(LIFE_SPAN_SECS - 300, TimeUnit.SECONDS)
+ .expireAfterWrite(LIFE_SPAN_SECS - CLOCK_SKEW, TimeUnit.SECONDS)
.ticker(
new Ticker() {
@Override
@@ -265,14 +263,43 @@ public long read() {
}
})
.build(
- new CacheLoader() {
+ new CacheLoader() {
@Override
- public String load(URI key) throws Exception {
- return generateJwtAccess(key);
+ public JwtCredentials load(JwtClaims claims) throws Exception {
+ return JwtCredentials.newBuilder()
+ .setPrivateKey(privateKey)
+ .setPrivateKeyId(privateKeyId)
+ .setJwtClaims(claims)
+ .setLifeSpanSeconds(LIFE_SPAN_SECS)
+ .setClock(clock)
+ .build();
}
});
}
+ /**
+ * Returns a new JwtCredentials instance with modified claims.
+ *
+ * @param newClaims new claims. Any unspecified claim fields will default to the the current
+ * values.
+ * @return new credentials
+ */
+ @Override
+ public JwtCredentials jwtWithClaims(JwtClaims newClaims) {
+ JwtClaims.Builder claimsBuilder =
+ JwtClaims.newBuilder().setIssuer(clientEmail).setSubject(clientEmail);
+ if (defaultAudience != null) {
+ claimsBuilder.setAudience(defaultAudience.toString());
+ }
+ return JwtCredentials.newBuilder()
+ .setPrivateKey(privateKey)
+ .setPrivateKeyId(privateKeyId)
+ .setJwtClaims(claimsBuilder.build().merge(newClaims))
+ .setLifeSpanSeconds(LIFE_SPAN_SECS)
+ .setClock(clock)
+ .build();
+ }
+
@Override
public String getAuthenticationType() {
return "JWTAccess";
@@ -308,21 +335,16 @@ public Map> getRequestMetadata(URI uri) throws IOException
+ "defaultAudience to be specified");
}
}
- String assertion = getJwtAccess(uri);
- String authorizationHeader = JWT_ACCESS_PREFIX + assertion;
- List newAuthorizationHeaders = Collections.singletonList(authorizationHeader);
- return Collections.singletonMap(AuthHttpConstants.AUTHORIZATION, newAuthorizationHeaders);
- }
- /** Discard any cached data */
- @Override
- public void refresh() {
- tokenCache.invalidateAll();
- }
-
- private String getJwtAccess(URI uri) throws IOException {
try {
- return tokenCache.get(uri);
+ JwtClaims defaultClaims =
+ JwtClaims.newBuilder()
+ .setAudience(uri.toString())
+ .setIssuer(clientEmail)
+ .setSubject(clientEmail)
+ .build();
+ JwtCredentials credentials = credentialsCache.get(defaultClaims);
+ return credentials.getRequestMetadata(uri);
} catch (ExecutionException e) {
Throwables.propagateIfPossible(e.getCause(), IOException.class);
// Should never happen
@@ -337,30 +359,10 @@ private String getJwtAccess(URI uri) throws IOException {
}
}
- private String generateJwtAccess(URI uri) throws IOException {
- JsonWebSignature.Header header = new JsonWebSignature.Header();
- header.setAlgorithm("RS256");
- header.setType("JWT");
- header.setKeyId(privateKeyId);
-
- JsonWebToken.Payload payload = new JsonWebToken.Payload();
- long currentTime = clock.currentTimeMillis();
- // Both copies of the email are required
- payload.setIssuer(clientEmail);
- payload.setSubject(clientEmail);
- payload.setAudience(uri.toString());
- payload.setIssuedAtTimeSeconds(currentTime / 1000);
- payload.setExpirationTimeSeconds(currentTime / 1000 + LIFE_SPAN_SECS);
-
- JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY;
-
- String assertion;
- try {
- assertion = JsonWebSignature.signUsingRsaSha256(privateKey, jsonFactory, header, payload);
- } catch (GeneralSecurityException e) {
- throw new IOException("Error signing service account JWT access header with private key.", e);
- }
- return assertion;
+ /** Discard any cached data */
+ @Override
+ public void refresh() {
+ credentialsCache.invalidateAll();
}
public final String getClientId() {
@@ -427,7 +429,7 @@ public boolean equals(Object obj) {
private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
input.defaultReadObject();
clock = Clock.SYSTEM;
- tokenCache = createCache();
+ credentialsCache = createCache();
}
public static Builder newBuilder() {
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/JwtCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/JwtCredentialsTest.java
new file mode 100644
index 000000000..53fdf34ba
--- /dev/null
+++ b/oauth2_http/javatests/com/google/auth/oauth2/JwtCredentialsTest.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright 2019, Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+import com.google.api.client.json.JsonFactory;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.client.json.webtoken.JsonWebSignature;
+import com.google.api.client.util.Clock;
+import com.google.auth.http.AuthHttpConstants;
+import java.io.IOException;
+import java.security.PrivateKey;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+
+public class JwtCredentialsTest extends BaseSerializationTest {
+ private static final String PRIVATE_KEY_ID = "d84a4fefcf50791d4a90f2d7af17469d6282df9d";
+ private static final String PRIVATE_KEY =
+ "-----BEGIN PRIVATE KEY-----\n"
+ + "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALX0PQoe1igW12i"
+ + "kv1bN/r9lN749y2ijmbc/mFHPyS3hNTyOCjDvBbXYbDhQJzWVUikh4mvGBA07qTj79Xc3yBDfKP2IeyYQIFe0t0"
+ + "zkd7R9Zdn98Y2rIQC47aAbDfubtkU1U72t4zL11kHvoa0/RuFZjncvlr42X7be7lYh4p3NAgMBAAECgYASk5wDw"
+ + "4Az2ZkmeuN6Fk/y9H+Lcb2pskJIXjrL533vrDWGOC48LrsThMQPv8cxBky8HFSEklPpkfTF95tpD43iVwJRB/Gr"
+ + "CtGTw65IfJ4/tI09h6zGc4yqvIo1cHX/LQ+SxKLGyir/dQM925rGt/VojxY5ryJR7GLbCzxPnJm/oQJBANwOCO6"
+ + "D2hy1LQYJhXh7O+RLtA/tSnT1xyMQsGT+uUCMiKS2bSKx2wxo9k7h3OegNJIu1q6nZ6AbxDK8H3+d0dUCQQDTrP"
+ + "SXagBxzp8PecbaCHjzNRSQE2in81qYnrAFNB4o3DpHyMMY6s5ALLeHKscEWnqP8Ur6X4PvzZecCWU9BKAZAkAut"
+ + "LPknAuxSCsUOvUfS1i87ex77Ot+w6POp34pEX+UWb+u5iFn2cQacDTHLV1LtE80L8jVLSbrbrlH43H0DjU5AkEA"
+ + "gidhycxS86dxpEljnOMCw8CKoUBd5I880IUahEiUltk7OLJYS/Ts1wbn3kPOVX3wyJs8WBDtBkFrDHW2ezth2QJ"
+ + "ADj3e1YhMVdjJW5jqwlD/VNddGjgzyunmiZg0uOXsHXbytYmsA545S8KRQFaJKFXYYFo2kOjqOiC1T2cAzMDjCQ"
+ + "==\n-----END PRIVATE KEY-----\n";
+ private static final String JWT_ACCESS_PREFIX =
+ ServiceAccountJwtAccessCredentials.JWT_ACCESS_PREFIX;
+ private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
+
+ static PrivateKey getPrivateKey() {
+ try {
+ return ServiceAccountCredentials.privateKeyFromPkcs8(PRIVATE_KEY);
+ } catch (IOException ex) {
+ return null;
+ }
+ }
+
+ @Test
+ public void serialize() throws IOException, ClassNotFoundException {
+ JwtClaims claims =
+ JwtClaims.newBuilder()
+ .setAudience("some-audience")
+ .setIssuer("some-issuer")
+ .setSubject("some-subject")
+ .build();
+ JwtCredentials credentials =
+ JwtCredentials.newBuilder()
+ .setJwtClaims(claims)
+ .setPrivateKey(getPrivateKey())
+ .setPrivateKeyId(PRIVATE_KEY_ID)
+ .build();
+
+ JwtCredentials deserializedCredentials = serializeAndDeserialize(credentials);
+ assertEquals(credentials, deserializedCredentials);
+ assertEquals(credentials.hashCode(), deserializedCredentials.hashCode());
+ assertEquals(credentials.toString(), deserializedCredentials.toString());
+ assertSame(deserializedCredentials.getClock(), Clock.SYSTEM);
+ }
+
+ @Test
+ public void builder_requiresPrivateKey() {
+ try {
+ JwtClaims claims =
+ JwtClaims.newBuilder()
+ .setAudience("some-audience")
+ .setIssuer("some-issuer")
+ .setSubject("some-subject")
+ .build();
+ JwtCredentials.newBuilder().setJwtClaims(claims).setPrivateKeyId(PRIVATE_KEY_ID).build();
+ fail("Should throw exception");
+ } catch (NullPointerException ex) {
+ // expected
+ }
+ }
+
+ @Test
+ public void builder_requiresPrivateKeyId() {
+ try {
+ JwtClaims claims =
+ JwtClaims.newBuilder()
+ .setAudience("some-audience")
+ .setIssuer("some-issuer")
+ .setSubject("some-subject")
+ .build();
+ JwtCredentials.newBuilder().setJwtClaims(claims).setPrivateKey(getPrivateKey()).build();
+ fail("Should throw exception");
+ } catch (NullPointerException ex) {
+ // expected
+ }
+ }
+
+ @Test
+ public void builder_requiresClaims() {
+ try {
+ JwtCredentials.newBuilder()
+ .setPrivateKeyId(PRIVATE_KEY_ID)
+ .setPrivateKey(getPrivateKey())
+ .build();
+ fail("Should throw exception");
+ } catch (NullPointerException ex) {
+ // expected
+ }
+ }
+
+ @Test
+ public void builder_requiresCompleteClaims() {
+ try {
+ JwtClaims claims = JwtClaims.newBuilder().build();
+ JwtCredentials.newBuilder()
+ .setJwtClaims(claims)
+ .setPrivateKeyId(PRIVATE_KEY_ID)
+ .setPrivateKey(getPrivateKey())
+ .build();
+ fail("Should throw exception");
+ } catch (IllegalStateException ex) {
+ // expected
+ }
+ }
+
+ @Test
+ public void claims_merge_overwritesFields() {
+ JwtClaims claims1 =
+ JwtClaims.newBuilder()
+ .setAudience("audience-1")
+ .setIssuer("issuer-1")
+ .setSubject("subject-1")
+ .build();
+ JwtClaims claims2 =
+ JwtClaims.newBuilder()
+ .setAudience("audience-2")
+ .setIssuer("issuer-2")
+ .setSubject("subject-2")
+ .build();
+ JwtClaims merged = claims1.merge(claims2);
+
+ assertEquals("audience-2", merged.getAudience());
+ assertEquals("issuer-2", merged.getIssuer());
+ assertEquals("subject-2", merged.getSubject());
+ }
+
+ @Test
+ public void claims_merge_defaultValues() {
+ JwtClaims claims1 =
+ JwtClaims.newBuilder()
+ .setAudience("audience-1")
+ .setIssuer("issuer-1")
+ .setSubject("subject-1")
+ .build();
+ JwtClaims claims2 = JwtClaims.newBuilder().setAudience("audience-2").build();
+ JwtClaims merged = claims1.merge(claims2);
+
+ assertEquals("audience-2", merged.getAudience());
+ assertEquals("issuer-1", merged.getIssuer());
+ assertEquals("subject-1", merged.getSubject());
+ }
+
+ @Test
+ public void claims_merge_null() {
+ JwtClaims claims1 = JwtClaims.newBuilder().build();
+ JwtClaims claims2 = JwtClaims.newBuilder().build();
+ JwtClaims merged = claims1.merge(claims2);
+
+ assertNull(merged.getAudience());
+ assertNull(merged.getIssuer());
+ assertNull(merged.getSubject());
+ }
+
+ @Test
+ public void claims_equals() {
+ JwtClaims claims1 =
+ JwtClaims.newBuilder()
+ .setAudience("audience-1")
+ .setIssuer("issuer-1")
+ .setSubject("subject-1")
+ .build();
+ JwtClaims claims2 =
+ JwtClaims.newBuilder()
+ .setAudience("audience-1")
+ .setIssuer("issuer-1")
+ .setSubject("subject-1")
+ .build();
+
+ assertEquals(claims1, claims2);
+ }
+
+ @Test
+ public void jwtWithClaims_overwritesClaims() throws IOException {
+ JwtClaims claims =
+ JwtClaims.newBuilder()
+ .setAudience("some-audience")
+ .setIssuer("some-issuer")
+ .setSubject("some-subject")
+ .build();
+ JwtCredentials credentials =
+ JwtCredentials.newBuilder()
+ .setJwtClaims(claims)
+ .setPrivateKey(getPrivateKey())
+ .setPrivateKeyId(PRIVATE_KEY_ID)
+ .build();
+ JwtClaims claims2 =
+ JwtClaims.newBuilder()
+ .setAudience("some-audience2")
+ .setIssuer("some-issuer2")
+ .setSubject("some-subject2")
+ .build();
+ JwtCredentials credentials2 = credentials.jwtWithClaims(claims2);
+ Map> metadata = credentials2.getRequestMetadata();
+ verifyJwtAccess(metadata, "some-audience2", "some-issuer2", "some-subject2", PRIVATE_KEY_ID);
+ }
+
+ @Test
+ public void jwtWithClaims_defaultsClaims() throws IOException {
+ JwtClaims claims =
+ JwtClaims.newBuilder()
+ .setAudience("some-audience")
+ .setIssuer("some-issuer")
+ .setSubject("some-subject")
+ .build();
+ JwtCredentials credentials =
+ JwtCredentials.newBuilder()
+ .setJwtClaims(claims)
+ .setPrivateKey(getPrivateKey())
+ .setPrivateKeyId(PRIVATE_KEY_ID)
+ .build();
+ JwtClaims claims2 = JwtClaims.newBuilder().build();
+ JwtCredentials credentials2 = credentials.jwtWithClaims(claims2);
+ Map> metadata = credentials2.getRequestMetadata();
+ verifyJwtAccess(metadata, "some-audience", "some-issuer", "some-subject", PRIVATE_KEY_ID);
+ }
+
+ @Test
+ public void getRequestMetadata_hasJwtAccess() throws IOException {
+ JwtClaims claims =
+ JwtClaims.newBuilder()
+ .setAudience("some-audience")
+ .setIssuer("some-issuer")
+ .setSubject("some-subject")
+ .build();
+ JwtCredentials credentials =
+ JwtCredentials.newBuilder()
+ .setJwtClaims(claims)
+ .setPrivateKey(getPrivateKey())
+ .setPrivateKeyId(PRIVATE_KEY_ID)
+ .build();
+
+ Map> metadata = credentials.getRequestMetadata();
+ verifyJwtAccess(metadata, "some-audience", "some-issuer", "some-subject", PRIVATE_KEY_ID);
+ }
+
+ private void verifyJwtAccess(
+ Map> metadata,
+ String expectedAudience,
+ String expectedIssuer,
+ String expectedSubject,
+ String expectedKeyId)
+ throws IOException {
+ assertNotNull(metadata);
+ List authorizations = metadata.get(AuthHttpConstants.AUTHORIZATION);
+ assertNotNull("Authorization headers not found", authorizations);
+ String assertion = null;
+ for (String authorization : authorizations) {
+ if (authorization.startsWith(JWT_ACCESS_PREFIX)) {
+ assertNull("Multiple bearer assertions found", assertion);
+ assertion = authorization.substring(JWT_ACCESS_PREFIX.length());
+ }
+ }
+ assertNotNull("Bearer assertion not found", assertion);
+ JsonWebSignature signature = JsonWebSignature.parse(JSON_FACTORY, assertion);
+ assertEquals(expectedIssuer, signature.getPayload().getIssuer());
+ assertEquals(expectedSubject, signature.getPayload().getSubject());
+ assertEquals(expectedAudience, signature.getPayload().getAudience());
+ assertEquals(expectedKeyId, signature.getHeader().getKeyId());
+ }
+}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountJwtAccessCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountJwtAccessCredentialsTest.java
index 0b9c10463..5435148c6 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountJwtAccessCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountJwtAccessCredentialsTest.java
@@ -680,6 +680,59 @@ public void fromStream_noPrivateKeyId_throws() throws IOException {
testFromStreamException(serviceAccountStream, "private_key_id");
}
+ @Test
+ public void jwtWithClaims_overrideAudience() throws IOException {
+ PrivateKey privateKey = ServiceAccountCredentials.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8);
+ ServiceAccountJwtAccessCredentials credentials =
+ ServiceAccountJwtAccessCredentials.newBuilder()
+ .setClientId(SA_CLIENT_ID)
+ .setClientEmail(SA_CLIENT_EMAIL)
+ .setPrivateKey(privateKey)
+ .setPrivateKeyId(SA_PRIVATE_KEY_ID)
+ .build();
+ Credentials withAudience =
+ credentials.jwtWithClaims(JwtClaims.newBuilder().setAudience("new-audience").build());
+
+ Map> metadata = withAudience.getRequestMetadata(CALL_URI);
+
+ verifyJwtAccess(metadata, SA_CLIENT_EMAIL, URI.create("new-audience"), SA_PRIVATE_KEY_ID);
+ }
+
+ @Test
+ public void jwtWithClaims_noAudience() throws IOException {
+ PrivateKey privateKey = ServiceAccountCredentials.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8);
+ ServiceAccountJwtAccessCredentials credentials =
+ ServiceAccountJwtAccessCredentials.newBuilder()
+ .setClientId(SA_CLIENT_ID)
+ .setClientEmail(SA_CLIENT_EMAIL)
+ .setPrivateKey(privateKey)
+ .setPrivateKeyId(SA_PRIVATE_KEY_ID)
+ .build();
+ try {
+ credentials.jwtWithClaims(JwtClaims.newBuilder().build());
+ fail("Expected to throw exception for missing audience");
+ } catch (IllegalStateException ex) {
+ // expected exception
+ }
+ }
+
+ @Test
+ public void jwtWithClaims_defaultAudience() throws IOException {
+ PrivateKey privateKey = ServiceAccountCredentials.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8);
+ ServiceAccountJwtAccessCredentials credentials =
+ ServiceAccountJwtAccessCredentials.newBuilder()
+ .setClientId(SA_CLIENT_ID)
+ .setClientEmail(SA_CLIENT_EMAIL)
+ .setPrivateKey(privateKey)
+ .setPrivateKeyId(SA_PRIVATE_KEY_ID)
+ .setDefaultAudience(URI.create("default-audience"))
+ .build();
+ Credentials withAudience = credentials.jwtWithClaims(JwtClaims.newBuilder().build());
+
+ Map> metadata = withAudience.getRequestMetadata(CALL_URI);
+ verifyJwtAccess(metadata, SA_CLIENT_EMAIL, URI.create("default-audience"), SA_PRIVATE_KEY_ID);
+ }
+
private void verifyJwtAccess(
Map> metadata,
String expectedEmail,
diff --git a/oauth2_http/pom.xml b/oauth2_http/pom.xml
index 1a5b735f5..541bba6b9 100644
--- a/oauth2_http/pom.xml
+++ b/oauth2_http/pom.xml
@@ -40,10 +40,30 @@
org.apache.maven.plugins
maven-javadoc-plugin
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+ com.google.auto.value:auto-value
+
+
+
+ com.google.auto.value
+ auto-value-annotations
+
+
+ com.google.auto.value
+ auto-value
+ provided
+
+
+ com.google.code.findbugs
+ jsr305
+
com.google.auth
google-auth-library-credentials
diff --git a/pom.xml b/pom.xml
index 776cd46db..9f3d156fa 100644
--- a/pom.xml
+++ b/pom.xml
@@ -61,6 +61,8 @@
4.12
28.0-android
1.9.74
+ 1.6.2
+ 3.0.2
false
@@ -98,6 +100,22 @@
guava
${project.guava.version}
+
+ com.google.auto.value
+ auto-value-annotations
+ ${project.autovalue.version}
+
+
+ com.google.auto.value
+ auto-value
+ ${project.autovalue.version}
+ provided
+
+
+ com.google.code.findbugs
+ jsr305
+ ${project.findbugs.version}
+
junit
junit
@@ -180,6 +198,11 @@
sponge_log
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ 3.1.1
+
com.coveo
fmt-maven-plugin
diff --git a/renovate.json b/renovate.json
index 68e63ff15..ca74999ac 100644
--- a/renovate.json
+++ b/renovate.json
@@ -6,6 +6,10 @@
{
"packagePatterns": ["^com.google.appengine:appengine-"],
"groupName": "AppEngine packages"
+ },
+ {
+ "packagePatterns": ["^com.google.auto.value:auto-"],
+ "groupName": "AutoValue packages"
}
]
}