Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow scopes for self signed jwt #689

Merged
merged 6 commits into from
Jul 14, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion oauth2_http/java/com/google/auth/oauth2/JwtClaims.java
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ public JwtClaims merge(JwtClaims other) {
* @return true if all required fields have been set; false otherwise
*/
public boolean isComplete() {
return getAudience() != null && getIssuer() != null && getSubject() != null;
boolean hasScopes =
getAdditionalClaims().containsKey("scope") && !getAdditionalClaims().get("scope").isEmpty();
return (getAudience() != null || hasScopes) && getIssuer() != null && getSubject() != null;
}

@AutoValue.Builder
Expand Down
110 changes: 81 additions & 29 deletions oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java
Original file line number Diff line number Diff line change
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;
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved

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);
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
}

/** 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);
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
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);
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
}

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

public boolean getAlwaysUseJwtAccess() {
return alwaysUseJwtAccess;
}

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

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

Expand All @@ -867,7 +878,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 +949,34 @@ String createAssertionForIdToken(
}
}

@VisibleForTesting
JwtCredentials createSelfSignedJwtCredentials(final 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.
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
blockingGetToCallback(uri, callback);
} else {
super.getRequestMetadata(uri, executor, callback);
}
Expand All @@ -950,14 +985,19 @@ 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()) {
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
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.");
}
}
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 +1037,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 +1054,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 +1119,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 +1172,10 @@ public int getLifetime() {
return lifetime;
}

public boolean getAlwaysUseJwtAccess() {
return alwaysUseJwtAccess;
}

public ServiceAccountCredentials build() {
return new ServiceAccountCredentials(
clientId,
Expand All @@ -1138,7 +1189,8 @@ public ServiceAccountCredentials build() {
serviceAccountUser,
projectId,
quotaProjectId,
lifetime);
lifetime,
alwaysUseJwtAccess);
}
}
}
Original file line number Diff line number Diff line change
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
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
* 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
12 changes: 12 additions & 0 deletions oauth2_http/javatests/com/google/auth/oauth2/JwtClaimsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,16 @@ public void testMergeAdditionalClaims() {
assertEquals("bar", mergedAdditionalClaims.get("foo"));
assertEquals("qwer", mergedAdditionalClaims.get("asdf"));
}

@Test
public void testIsComplete() {
// Test JwtClaim is complete if audience is not set but scope is provided.
JwtClaims claims =
JwtClaims.newBuilder()
.setIssuer("issuer-1")
.setSubject("subject-1")
.setAdditionalClaims(Collections.singletonMap("scope", "foo"))
.build();
assertTrue(claims.isComplete());
}
}
Loading