Skip to content

Commit

Permalink
feat: allow scopes for self signed jwt
Browse files Browse the repository at this point in the history
  • Loading branch information
arithmetic1728 committed Jun 22, 2021
1 parent 5a8f467 commit 3aa63b8
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 88 deletions.
113 changes: 84 additions & 29 deletions oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java
Expand Up @@ -77,6 +77,7 @@
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -109,9 +110,9 @@ public class ServiceAccountCredentials extends GoogleCredentials
private final Collection<String> defaultScopes;
private final String quotaProjectId;
private final int lifetime;
private final boolean alwaysUseJwtAccess;

private transient HttpTransportFactory transportFactory;
private transient ServiceAccountJwtAccessCredentials jwtCredentials = null;

/**
* Constructor with minimum identifying information and custom HTTP transport.
Expand All @@ -133,6 +134,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
* most 43200 (12 hours). If the token is used for calling a Google API, then the value should
* be at most 3600 (1 hour). If the given value is 0, then the default value 3600 will be used
* when creating the credentials.
* @param alwaysUseJwtAccess whether self signed JWT should be always used.
*/
ServiceAccountCredentials(
String clientId,
Expand All @@ -146,7 +148,8 @@ public class ServiceAccountCredentials extends GoogleCredentials
String serviceAccountUser,
String projectId,
String quotaProjectId,
int lifetime) {
int lifetime,
boolean alwaysUseJwtAccess) {
this.clientId = clientId;
this.clientEmail = Preconditions.checkNotNull(clientEmail);
this.privateKey = Preconditions.checkNotNull(privateKey);
Expand All @@ -167,18 +170,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
throw new IllegalStateException("lifetime must be less than or equal to 43200");
}
this.lifetime = lifetime;

// Use self signed JWT if scopes is not set, see https://google.aip.dev/auth/4111.
if (this.scopes.isEmpty()) {
jwtCredentials =
new ServiceAccountJwtAccessCredentials.Builder()
.setClientEmail(clientEmail)
.setClientId(clientId)
.setPrivateKey(privateKey)
.setPrivateKeyId(privateKeyId)
.setQuotaProjectId(quotaProjectId)
.build();
}
this.alwaysUseJwtAccess = alwaysUseJwtAccess;
}

/**
Expand Down Expand Up @@ -492,7 +484,8 @@ static ServiceAccountCredentials fromPkcs8(
serviceAccountUser,
projectId,
quotaProject,
DEFAULT_LIFETIME_IN_SECONDS);
DEFAULT_LIFETIME_IN_SECONDS,
false);
}

/** Helper to convert from a PKCS#8 String to an RSA private key */
Expand Down Expand Up @@ -698,7 +691,8 @@ public GoogleCredentials createScoped(
serviceAccountUser,
projectId,
quotaProjectId,
lifetime);
lifetime,
alwaysUseJwtAccess);
}

/**
Expand All @@ -714,6 +708,16 @@ public ServiceAccountCredentials createWithCustomLifetime(int lifetime) {
return this.toBuilder().setLifetime(lifetime).build();
}

/**
* Clones the service account with a new alwaysUseJwtAccess value.
*
* @param alwaysUseJwtAccess whether self signed JWT should be used
* @return the cloned service account credentials with the given alwaysUseJwtAccess
*/
public ServiceAccountCredentials createWithAlwaysUseJwtAccess(boolean alwaysUseJwtAccess) {
return this.toBuilder().setAlwaysUseJwtAccess(alwaysUseJwtAccess).build();
}

@Override
public GoogleCredentials createDelegated(String user) {
return new ServiceAccountCredentials(
Expand All @@ -728,7 +732,8 @@ public GoogleCredentials createDelegated(String user) {
user,
projectId,
quotaProjectId,
lifetime);
lifetime,
alwaysUseJwtAccess);
}

public final String getClientId() {
Expand Down Expand Up @@ -776,6 +781,11 @@ int getLifetime() {
return lifetime;
}

@VisibleForTesting
boolean getAlwaysUseJwtAccess() {
return alwaysUseJwtAccess;
}

@Override
public String getAccount() {
return getClientEmail();
Expand Down Expand Up @@ -833,7 +843,8 @@ public int hashCode() {
scopes,
defaultScopes,
quotaProjectId,
lifetime);
lifetime,
alwaysUseJwtAccess);
}

@Override
Expand All @@ -849,6 +860,7 @@ public String toString() {
.add("serviceAccountUser", serviceAccountUser)
.add("quotaProjectId", quotaProjectId)
.add("lifetime", lifetime)
.add("alwaysUseJwtAccess", alwaysUseJwtAccess)
.toString();
}

Expand All @@ -867,7 +879,8 @@ public boolean equals(Object obj) {
&& Objects.equals(this.scopes, other.scopes)
&& Objects.equals(this.defaultScopes, other.defaultScopes)
&& Objects.equals(this.quotaProjectId, other.quotaProjectId)
&& Objects.equals(this.lifetime, other.lifetime);
&& Objects.equals(this.lifetime, other.lifetime)
&& Objects.equals(this.alwaysUseJwtAccess, other.alwaysUseJwtAccess);
}

String createAssertion(JsonFactory jsonFactory, long currentTime, String audience)
Expand Down Expand Up @@ -937,11 +950,33 @@ String createAssertionForIdToken(
}
}

@VisibleForTesting
JwtCredentials createSelfSignedJwtCredentials(URI uri) {
// Create a JwtCredentials for self signed JWT. See https://google.aip.dev/auth/4111.
JwtClaims.Builder claimsBuilder =
JwtClaims.newBuilder().setIssuer(clientEmail).setSubject(clientEmail);
if (!scopes.isEmpty()) {
claimsBuilder.setAdditionalClaims(Collections.singletonMap("scope", Joiner.on(' ').join(scopes)));
} else if (uri != null) {
claimsBuilder.setAudience(uri.toString());
} else {
claimsBuilder.setAdditionalClaims(Collections.singletonMap("scope", Joiner.on(' ').join(defaultScopes)));
}
return JwtCredentials.newBuilder()
.setPrivateKey(privateKey)
.setPrivateKeyId(privateKeyId)
.setJwtClaims(claimsBuilder.build())
.setClock(clock)
.build();
}


@Override
public void getRequestMetadata(
final URI uri, Executor executor, final RequestMetadataCallback callback) {
if (jwtCredentials != null && uri != null) {
jwtCredentials.getRequestMetadata(uri, executor, callback);
if (alwaysUseJwtAccess) {
// This will call getRequestMetadata(URI uri), which handles self signed JWT logic.
blockingGetToCallback(uri, callback);
} else {
super.getRequestMetadata(uri, executor, callback);
}
Expand All @@ -950,14 +985,22 @@ public void getRequestMetadata(
/** Provide the request metadata by putting an access JWT directly in the metadata. */
@Override
public Map<String, List<String>> getRequestMetadata(URI uri) throws IOException {
if (scopes.isEmpty() && defaultScopes.isEmpty() && uri == null) {
throw new IOException(
"Scopes and uri are not configured for service account. Either pass uri"
+ " to getRequestMetadata to use self signed JWT, or specify the scopes"
+ " by calling createScoped or passing scopes to constructor.");
if (createScopedRequired()) {
if (!alwaysUseJwtAccess) {
throw new IOException(
"Scopes are not configured for service account. Specify the scopes"
+ " by calling createScoped or passing scopes to constructor.");
} else if (uri == null) {
throw new IOException(
"Scopes and uri are not configured for service account. Either pass uri"
+ " to getRequestMetadata, or specify the scopes by calling createScoped"
+ " or passing scopes to constructor.");
}
}
if (jwtCredentials != null && uri != null) {
return jwtCredentials.getRequestMetadata(uri);
if (alwaysUseJwtAccess) {
JwtCredentials jwtCredentials = createSelfSignedJwtCredentials(uri);
Map<String, List<String>> requestMetadata = jwtCredentials.getRequestMetadata(uri);
return addQuotaProjectIdToRequestMetadata(quotaProjectId, requestMetadata);
} else {
return super.getRequestMetadata(uri);
}
Expand Down Expand Up @@ -997,6 +1040,7 @@ public static class Builder extends GoogleCredentials.Builder {
private HttpTransportFactory transportFactory;
private String quotaProjectId;
private int lifetime = DEFAULT_LIFETIME_IN_SECONDS;
private boolean alwaysUseJwtAccess = false;

protected Builder() {}

Expand All @@ -1013,6 +1057,7 @@ protected Builder(ServiceAccountCredentials credentials) {
this.projectId = credentials.projectId;
this.quotaProjectId = credentials.quotaProjectId;
this.lifetime = credentials.lifetime;
this.alwaysUseJwtAccess = credentials.alwaysUseJwtAccess;
}

public Builder setClientId(String clientId) {
Expand Down Expand Up @@ -1077,6 +1122,11 @@ public Builder setLifetime(int lifetime) {
return this;
}

public Builder setAlwaysUseJwtAccess(boolean alwaysUseJwtAccess) {
this.alwaysUseJwtAccess = alwaysUseJwtAccess;
return this;
}

public String getClientId() {
return clientId;
}
Expand Down Expand Up @@ -1125,6 +1175,10 @@ public int getLifetime() {
return lifetime;
}

public boolean getAlwaysUseJwtAccess() {
return alwaysUseJwtAccess;
}

public ServiceAccountCredentials build() {
return new ServiceAccountCredentials(
clientId,
Expand All @@ -1138,7 +1192,8 @@ public ServiceAccountCredentials build() {
serviceAccountUser,
projectId,
quotaProjectId,
lifetime);
lifetime,
alwaysUseJwtAccess);
}
}
}
Expand Up @@ -54,7 +54,6 @@
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
Expand Down Expand Up @@ -332,35 +331,17 @@ public boolean hasRequestMetadataOnly() {
return true;
}

/**
* Self signed JWT uses uri as audience, which should have the "https://{host}/" format. For
* instance, if the uri is "https://compute.googleapis.com/compute/v1/projects/", then this
* function returns "https://compute.googleapis.com/".
*/
@VisibleForTesting
static URI getUriForSelfSignedJWT(URI uri) {
if (uri == null || uri.getScheme() == null || uri.getHost() == null) {
return uri;
}
try {
return new URI(uri.getScheme(), uri.getHost(), "/", null);
} catch (URISyntaxException unused) {
return uri;
}
}

@Override
public void getRequestMetadata(
final URI uri, Executor executor, final RequestMetadataCallback callback) {
// It doesn't use network. Only some CPU work on par with TLS handshake. So it's preferrable
// to do it in the current thread, which is likely to be the network thread.
blockingGetToCallback(getUriForSelfSignedJWT(uri), callback);
blockingGetToCallback(uri, callback);
}

/** Provide the request metadata by putting an access JWT directly in the metadata. */
@Override
public Map<String, List<String>> getRequestMetadata(URI uri) throws IOException {
uri = getUriForSelfSignedJWT(uri);
if (uri == null) {
if (defaultAudience != null) {
uri = defaultAudience;
Expand Down
Expand Up @@ -106,7 +106,7 @@ public class ServiceAccountCredentialsTest extends BaseSerializationTest {
private static final String PROJECT_ID = "project-id";
private static final Collection<String> EMPTY_SCOPES = Collections.emptyList();
private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo");
private static final String JWT_AUDIENCE = "http://googleapis.com/";
private static final String JWT_AUDIENCE = "http://googleapis.com/testapi/v1/foo";
private static final HttpTransportFactory DUMMY_TRANSPORT_FACTORY =
new MockTokenServerTransportFactory();
public static final String DEFAULT_ID_TOKEN =
Expand Down Expand Up @@ -419,21 +419,16 @@ public void createdScoped_enablesAccessTokens() throws IOException {
null);

try {
credentials.getRequestMetadata(null);
fail("Should not be able to get token without scopes, defaultScopes and uri");
credentials.getRequestMetadata(CALL_URI);
fail("Should not be able to get token without scopes");
} catch (IOException e) {
assertTrue(
"expected to fail with exception",
e.getMessage().contains("Scopes and uri are not configured for service account"));
e.getMessage().contains("Scopes are not configured for service account"));
}

// Since scopes are not provided, self signed JWT will be used.
Map<String, List<String>> metadata = credentials.getRequestMetadata(CALL_URI);
verifyJwtAccess(metadata);

// Since scopes are provided, self signed JWT will not be used.
GoogleCredentials scopedCredentials = credentials.createScoped(SCOPES);
metadata = scopedCredentials.getRequestMetadata(CALL_URI);
Map<String, List<String>> metadata = scopedCredentials.getRequestMetadata(CALL_URI);
TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN);
}

Expand Down Expand Up @@ -1074,7 +1069,7 @@ public void toString_containsFields() throws IOException {
String.format(
"ServiceAccountCredentials{clientId=%s, clientEmail=%s, privateKeyId=%s, "
+ "transportFactoryClassName=%s, tokenServerUri=%s, scopes=%s, defaultScopes=%s, serviceAccountUser=%s, "
+ "quotaProjectId=%s, lifetime=3600}",
+ "quotaProjectId=%s, lifetime=3600, alwaysUseJwtAccess=false}",
CLIENT_ID,
CLIENT_EMAIL,
PRIVATE_KEY_ID,
Expand Down Expand Up @@ -1340,11 +1335,11 @@ public void getRequestMetadataWithCallback_selfSignedJWT() throws IOException {
.setClientEmail(CLIENT_EMAIL)
.setPrivateKey(privateKey)
.setPrivateKeyId(PRIVATE_KEY_ID)
.setScopes(null, DEFAULT_SCOPES)
.setServiceAccountUser(USER)
.setProjectId(PROJECT_ID)
.setQuotaProjectId("my-quota-project-id")
.setHttpTransportFactory(transportFactory)
.setAlwaysUseJwtAccess(true)
.build();

final AtomicBoolean success = new AtomicBoolean(false);
Expand Down

0 comments on commit 3aa63b8

Please sign in to comment.