Skip to content

Commit

Permalink
allow to use uri as audience
Browse files Browse the repository at this point in the history
  • Loading branch information
arithmetic1728 committed Jul 12, 2021
1 parent 9273ff3 commit c3f9740
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 54 deletions.
121 changes: 77 additions & 44 deletions oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java
Expand Up @@ -110,7 +110,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
private final Collection<String> defaultScopes;
private final String quotaProjectId;
private final int lifetime;
private final boolean useJWTAccessWithScope;
private final boolean useJwtAccessWithScope;

private transient HttpTransportFactory transportFactory;

Expand All @@ -134,7 +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.
* @param useJwtAccessWithScope whether self signed JWT with scopes should be always used.
*/
ServiceAccountCredentials(
String clientId,
Expand All @@ -149,7 +149,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
String projectId,
String quotaProjectId,
int lifetime,
boolean useJWTAccessWithScope) {
boolean useJwtAccessWithScope) {
this.clientId = clientId;
this.clientEmail = Preconditions.checkNotNull(clientEmail);
this.privateKey = Preconditions.checkNotNull(privateKey);
Expand All @@ -170,7 +170,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
throw new IllegalStateException("lifetime must be less than or equal to 43200");
}
this.lifetime = lifetime;
this.useJWTAccessWithScope = useJWTAccessWithScope;
this.useJwtAccessWithScope = useJwtAccessWithScope;
}

/**
Expand Down Expand Up @@ -692,7 +692,7 @@ public GoogleCredentials createScoped(
projectId,
quotaProjectId,
lifetime,
useJWTAccessWithScope);
useJwtAccessWithScope);
}

/**
Expand All @@ -709,13 +709,13 @@ public ServiceAccountCredentials createWithCustomLifetime(int lifetime) {
}

/**
* Clones the service account with a new useJWTAccessWithScope value.
* 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
* @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();
public ServiceAccountCredentials createWithUseJwtAccessWithScope(boolean useJwtAccessWithScope) {
return this.toBuilder().setUseJwtAccessWithScope(useJwtAccessWithScope).build();
}

@Override
Expand All @@ -733,7 +733,7 @@ public GoogleCredentials createDelegated(String user) {
projectId,
quotaProjectId,
lifetime,
useJWTAccessWithScope);
useJwtAccessWithScope);
}

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

public boolean getUseJWTAccessWithScope() {
return useJWTAccessWithScope;
public boolean getUseJwtAccessWithScope() {
return useJwtAccessWithScope;
}

@Override
Expand Down Expand Up @@ -843,7 +843,7 @@ public int hashCode() {
defaultScopes,
quotaProjectId,
lifetime,
useJWTAccessWithScope);
useJwtAccessWithScope);
}

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

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

String createAssertion(JsonFactory jsonFactory, long currentTime, String audience)
Expand Down Expand Up @@ -949,19 +949,41 @@ 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 (!scopes.isEmpty()) {
claimsBuilder.setAdditionalClaims(
Collections.singletonMap("scope", Joiner.on(' ').join(scopes)));
} else if (uri != null) {
claimsBuilder.setAudience(uri.toString());

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 {
claimsBuilder.setAdditionalClaims(
Collections.singletonMap("scope", Joiner.on(' ').join(defaultScopes)));
// otherwise, use audience with the uri.
claimsBuilder.setAudience(getUriForSelfSignedJWT(uri).toString());
}
return JwtCredentials.newBuilder()
.setPrivateKey(privateKey)
Expand All @@ -974,8 +996,10 @@ JwtCredentials createSelfSignedJwtCredentials(final URI uri) {
@Override
public void getRequestMetadata(
final URI uri, Executor executor, final RequestMetadataCallback callback) {
if (useJWTAccessWithScope) {
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 @@ -985,22 +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 (createScopedRequired()) {
if (!useJWTAccessWithScope) {
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 (createScopedRequired() && uri == null) {
throw new IOException(
"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 (useJWTAccessWithScope) {
JwtCredentials jwtCredentials = createSelfSignedJwtCredentials(uri);
Map<String, List<String>> requestMetadata = jwtCredentials.getRequestMetadata(uri);
return addQuotaProjectIdToRequestMetadata(quotaProjectId, requestMetadata);
} else {

// If scopes are provided but we cannot use self signed JWT, then use scopes in the normal oauth
// way.
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 @@ -1037,7 +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;
private boolean useJwtAccessWithScope = false;

protected Builder() {}

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

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

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

Expand Down Expand Up @@ -1172,8 +1205,8 @@ public int getLifetime() {
return lifetime;
}

public boolean getUseJWTAccessWithScope() {
return useJWTAccessWithScope;
public boolean getUseJwtAccessWithScope() {
return useJwtAccessWithScope;
}

public ServiceAccountCredentials build() {
Expand All @@ -1190,7 +1223,7 @@ public ServiceAccountCredentials build() {
projectId,
quotaProjectId,
lifetime,
useJWTAccessWithScope);
useJwtAccessWithScope);
}
}
}
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/testapi/v1/foo";
private static final String JWT_AUDIENCE = "http://googleapis.com/";
private static final HttpTransportFactory DUMMY_TRANSPORT_FACTORY =
new MockTokenServerTransportFactory();
public static final String DEFAULT_ID_TOKEN =
Expand Down Expand Up @@ -419,12 +419,12 @@ public void createdScoped_enablesAccessTokens() throws IOException {
null);

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

GoogleCredentials scopedCredentials = credentials.createScoped(SCOPES);
Expand Down Expand Up @@ -1069,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, useJWTAccessWithScope=false}",
+ "quotaProjectId=%s, lifetime=3600, useJwtAccessWithScope=false}",
CLIENT_ID,
CLIENT_EMAIL,
PRIVATE_KEY_ID,
Expand Down Expand Up @@ -1231,6 +1231,29 @@ public void fromStream_noPrivateKeyId_throws() throws IOException {
testFromStreamException(serviceAccountStream, "private_key_id");
}

@Test
public void getUriForSelfSignedJWT() {
assertNull(ServiceAccountCredentials.getUriForSelfSignedJWT(null));

URI uri = URI.create("https://compute.googleapis.com/compute/v1/projects/");
URI expected = URI.create("https://compute.googleapis.com/");
assertEquals(expected, ServiceAccountCredentials.getUriForSelfSignedJWT(uri));
}

@Test
public void getUriForSelfSignedJWT_noHost() {
URI uri = URI.create("file:foo");
URI expected = URI.create("file:foo");
assertEquals(expected, ServiceAccountCredentials.getUriForSelfSignedJWT(uri));
}

@Test
public void getUriForSelfSignedJWT_forStaticAudience_returnsURI() {
URI uri = URI.create("compute.googleapis.com");
URI expected = URI.create("compute.googleapis.com");
assertEquals(expected, ServiceAccountCredentials.getUriForSelfSignedJWT(uri));
}

@Test
public void getRequestMetadataSetsQuotaProjectId() throws IOException {
MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory();
Expand Down Expand Up @@ -1335,7 +1358,7 @@ public void getRequestMetadata_selfSignedJWT_withScopes() throws IOException {
.setServiceAccountUser(USER)
.setProjectId(PROJECT_ID)
.setHttpTransportFactory(new MockTokenServerTransportFactory())
.setUseJWTAccessWithScope(true)
.setUseJwtAccessWithScope(true)
.build();

Map<String, List<String>> metadata = credentials.getRequestMetadata(CALL_URI);
Expand All @@ -1351,11 +1374,9 @@ public void getRequestMetadata_selfSignedJWT_withAudience() throws IOException {
.setClientEmail(CLIENT_EMAIL)
.setPrivateKey(privateKey)
.setPrivateKeyId(PRIVATE_KEY_ID)
.setScopes(null, SCOPES)
.setServiceAccountUser(USER)
.setProjectId(PROJECT_ID)
.setHttpTransportFactory(new MockTokenServerTransportFactory())
.setUseJWTAccessWithScope(true)
.build();

Map<String, List<String>> metadata = credentials.getRequestMetadata(CALL_URI);
Expand All @@ -1375,7 +1396,7 @@ public void getRequestMetadata_selfSignedJWT_withDefaultScopes() throws IOExcept
.setServiceAccountUser(USER)
.setProjectId(PROJECT_ID)
.setHttpTransportFactory(new MockTokenServerTransportFactory())
.setUseJWTAccessWithScope(true)
.setUseJwtAccessWithScope(true)
.build();

Map<String, List<String>> metadata = credentials.getRequestMetadata(null);
Expand All @@ -1395,7 +1416,8 @@ public void getRequestMetadataWithCallback_selfSignedJWT() throws IOException {
.setProjectId(PROJECT_ID)
.setQuotaProjectId("my-quota-project-id")
.setHttpTransportFactory(new MockTokenServerTransportFactory())
.setUseJWTAccessWithScope(true)
.setUseJwtAccessWithScope(true)
.setScopes(SCOPES)
.build();

final AtomicBoolean success = new AtomicBoolean(false);
Expand All @@ -1406,7 +1428,7 @@ public void getRequestMetadataWithCallback_selfSignedJWT() throws IOException {
@Override
public void onSuccess(Map<String, List<String>> metadata) {
try {
verifyJwtAccess(metadata, null);
verifyJwtAccess(metadata, "dummy.scope");
} catch (IOException e) {
fail("Should not throw a failure");
}
Expand Down

0 comments on commit c3f9740

Please sign in to comment.