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

test: adds integration tests for downscoping with credential access boundaries #698

Merged
merged 5 commits into from
Jul 22, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [0.27.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.26.0...v0.27.0) (2021-07-14)


### Features

* add Id token support for UserCredentials ([#650](https://www.github.com/googleapis/google-auth-library-java/issues/650)) ([5a8f467](https://www.github.com/googleapis/google-auth-library-java/commit/5a8f4676630854c53aa708a9c8b960770067f858))
* add impersonation credentials to ADC ([#613](https://www.github.com/googleapis/google-auth-library-java/issues/613)) ([b9823f7](https://www.github.com/googleapis/google-auth-library-java/commit/b9823f70d7f3f7461b7de40bee06f5e7ba0e797c))
* Adding functional tests for Service Account ([#685](https://www.github.com/googleapis/google-auth-library-java/issues/685)) ([dfe118c](https://www.github.com/googleapis/google-auth-library-java/commit/dfe118c261aadf137a3cf47a7acb9892c7a6db4d))
* allow scopes for self signed jwt ([#689](https://www.github.com/googleapis/google-auth-library-java/issues/689)) ([f4980c7](https://www.github.com/googleapis/google-auth-library-java/commit/f4980c77566bbd5ef4c532acb199d7d484dbcd01))

## [0.26.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.25.5...v0.26.0) (2021-05-20)


Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ If you are using Maven, add this to your pom.xml file (notice that you can repla
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<version>0.26.0</version>
<version>0.27.0</version>
</dependency>
```
[//]: # ({x-version-update-end})
Expand All @@ -41,15 +41,15 @@ If you are using Gradle, add this to your dependencies

[//]: # ({x-version-update-start:google-auth-library-oauth2-http:released})
```Groovy
compile 'com.google.auth:google-auth-library-oauth2-http:0.26.0'
compile 'com.google.auth:google-auth-library-oauth2-http:0.27.0'
```
[//]: # ({x-version-update-end})

If you are using SBT, add this to your dependencies

[//]: # ({x-version-update-start:google-auth-library-oauth2-http:released})
```Scala
libraryDependencies += "com.google.auth" % "google-auth-library-oauth2-http" % "0.26.0"
libraryDependencies += "com.google.auth" % "google-auth-library-oauth2-http" % "0.27.0"
```
[//]: # ({x-version-update-end})

Expand Down
2 changes: 1 addition & 1 deletion appengine/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-parent</artifactId>
<version>0.26.1-SNAPSHOT</version><!-- {x-version-update:google-auth-library-parent:current} -->
<version>0.27.0</version><!-- {x-version-update:google-auth-library-parent:current} -->
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 1 addition & 1 deletion bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-bom</artifactId>
<version>0.26.1-SNAPSHOT</version><!-- {x-version-update:google-auth-library-bom:current} -->
<version>0.27.0</version><!-- {x-version-update:google-auth-library-bom:current} -->
<packaging>pom</packaging>
<name>Google Auth Library for Java BOM</name>
<description>
Expand Down
2 changes: 1 addition & 1 deletion credentials/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-parent</artifactId>
<version>0.26.1-SNAPSHOT</version><!-- {x-version-update:google-auth-library-parent:current} -->
<version>0.27.0</version><!-- {x-version-update:google-auth-library-parent:current} -->
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
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
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 useJwtAccessWithScope;

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 useJwtAccessWithScope whether self signed JWT with scopes 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 useJwtAccessWithScope) {
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.useJwtAccessWithScope = useJwtAccessWithScope;
}

/**
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,
useJwtAccessWithScope);
}

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

/**
* Clones the service account with a new useJwtAccessWithScope value.
*
* @param useJwtAccessWithScope whether self signed JWT with scopes should be used
* @return the cloned service account credentials with the given useJwtAccessWithScope
*/
public ServiceAccountCredentials createWithUseJwtAccessWithScope(boolean useJwtAccessWithScope) {
return this.toBuilder().setUseJwtAccessWithScope(useJwtAccessWithScope).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,
useJwtAccessWithScope);
}

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

public boolean getUseJwtAccessWithScope() {
return useJwtAccessWithScope;
}

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

@Override
Expand All @@ -849,6 +859,7 @@ public String toString() {
.add("serviceAccountUser", serviceAccountUser)
.add("quotaProjectId", quotaProjectId)
.add("lifetime", lifetime)
.add("useJwtAccessWithScope", useJwtAccessWithScope)
.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.useJwtAccessWithScope, other.useJwtAccessWithScope);
}

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

/**
* 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;
}
}

@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 (uri == null) {
// If uri is null, use scopes.
String scopeClaim = "";
if (!scopes.isEmpty()) {
scopeClaim = Joiner.on(' ').join(scopes);
} else {
scopeClaim = Joiner.on(' ').join(defaultScopes);
}
claimsBuilder.setAdditionalClaims(Collections.singletonMap("scope", scopeClaim));
} else {
// otherwise, use audience with the uri.
claimsBuilder.setAudience(getUriForSelfSignedJWT(uri).toString());
}
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 (useJwtAccessWithScope) {
// This will call getRequestMetadata(URI uri), which handles self signed JWT logic.
// Self signed JWT doesn't use network, so here we do a blocking call to improve
// efficiency. executor will be ignored since it is intended for async operation.
blockingGetToCallback(uri, callback);
} else {
super.getRequestMetadata(uri, executor, callback);
}
Expand All @@ -950,17 +1009,31 @@ 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) {
if (createScopedRequired() && 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.");
"Scopes and uri are not configured for service account. Specify the scopes"
+ " by calling createScoped or passing scopes to constructor or"
+ " providing uri to getRequestMetadata.");
}
if (jwtCredentials != null && uri != null) {
return jwtCredentials.getRequestMetadata(uri);
} else {

// If scopes are provided but we cannot use self signed JWT, then use scopes to get access
// token.
if (!createScopedRequired() && !useJwtAccessWithScope) {
return super.getRequestMetadata(uri);
}

// If scopes are provided and self signed JWT can be used, use self signed JWT with scopes.
// Otherwise, use self signed JWT with uri as the audience.
JwtCredentials jwtCredentials;
if (!createScopedRequired() && useJwtAccessWithScope) {
// Create JWT credentials with the scopes.
jwtCredentials = createSelfSignedJwtCredentials(null);
} else {
// Create JWT credentials with the uri as audience.
jwtCredentials = createSelfSignedJwtCredentials(uri);
}
Map<String, List<String>> requestMetadata = jwtCredentials.getRequestMetadata(null);
return addQuotaProjectIdToRequestMetadata(quotaProjectId, requestMetadata);
}

@SuppressWarnings("unused")
Expand Down Expand Up @@ -997,6 +1070,7 @@ public static class Builder extends GoogleCredentials.Builder {
private HttpTransportFactory transportFactory;
private String quotaProjectId;
private int lifetime = DEFAULT_LIFETIME_IN_SECONDS;
private boolean useJwtAccessWithScope = false;

protected Builder() {}

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

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

public Builder setUseJwtAccessWithScope(boolean useJwtAccessWithScope) {
this.useJwtAccessWithScope = useJwtAccessWithScope;
return this;
}

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

public boolean getUseJwtAccessWithScope() {
return useJwtAccessWithScope;
}

public ServiceAccountCredentials build() {
return new ServiceAccountCredentials(
clientId,
Expand All @@ -1138,7 +1222,8 @@ public ServiceAccountCredentials build() {
serviceAccountUser,
projectId,
quotaProjectId,
lifetime);
lifetime,
useJwtAccessWithScope);
}
}
}
Loading