Skip to content

Commit 0382367

Browse files
allow to use uri as audience
1 parent 9273ff3 commit 0382367

3 files changed

Lines changed: 113 additions & 58 deletions

File tree

oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java

Lines changed: 77 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
110110
private final Collection<String> defaultScopes;
111111
private final String quotaProjectId;
112112
private final int lifetime;
113-
private final boolean useJWTAccessWithScope;
113+
private final boolean useJwtAccessWithScope;
114114

115115
private transient HttpTransportFactory transportFactory;
116116

@@ -134,7 +134,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
134134
* most 43200 (12 hours). If the token is used for calling a Google API, then the value should
135135
* be at most 3600 (1 hour). If the given value is 0, then the default value 3600 will be used
136136
* when creating the credentials.
137-
* @param useJWTAccessWithScope whether self signed JWT with scopes should be always used.
137+
* @param useJwtAccessWithScope whether self signed JWT with scopes should be always used.
138138
*/
139139
ServiceAccountCredentials(
140140
String clientId,
@@ -149,7 +149,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
149149
String projectId,
150150
String quotaProjectId,
151151
int lifetime,
152-
boolean useJWTAccessWithScope) {
152+
boolean useJwtAccessWithScope) {
153153
this.clientId = clientId;
154154
this.clientEmail = Preconditions.checkNotNull(clientEmail);
155155
this.privateKey = Preconditions.checkNotNull(privateKey);
@@ -170,7 +170,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
170170
throw new IllegalStateException("lifetime must be less than or equal to 43200");
171171
}
172172
this.lifetime = lifetime;
173-
this.useJWTAccessWithScope = useJWTAccessWithScope;
173+
this.useJwtAccessWithScope = useJwtAccessWithScope;
174174
}
175175

176176
/**
@@ -692,7 +692,7 @@ public GoogleCredentials createScoped(
692692
projectId,
693693
quotaProjectId,
694694
lifetime,
695-
useJWTAccessWithScope);
695+
useJwtAccessWithScope);
696696
}
697697

698698
/**
@@ -709,13 +709,13 @@ public ServiceAccountCredentials createWithCustomLifetime(int lifetime) {
709709
}
710710

711711
/**
712-
* Clones the service account with a new useJWTAccessWithScope value.
712+
* Clones the service account with a new useJwtAccessWithScope value.
713713
*
714-
* @param useJWTAccessWithScope whether self signed JWT with scopes should be used
715-
* @return the cloned service account credentials with the given useJWTAccessWithScope
714+
* @param useJwtAccessWithScope whether self signed JWT with scopes should be used
715+
* @return the cloned service account credentials with the given useJwtAccessWithScope
716716
*/
717-
public ServiceAccountCredentials createWithUseJWTAccessWithScope(boolean useJWTAccessWithScope) {
718-
return this.toBuilder().setUseJWTAccessWithScope(useJWTAccessWithScope).build();
717+
public ServiceAccountCredentials createWithUseJwtAccessWithScope(boolean useJwtAccessWithScope) {
718+
return this.toBuilder().setUseJwtAccessWithScope(useJwtAccessWithScope).build();
719719
}
720720

721721
@Override
@@ -733,7 +733,7 @@ public GoogleCredentials createDelegated(String user) {
733733
projectId,
734734
quotaProjectId,
735735
lifetime,
736-
useJWTAccessWithScope);
736+
useJwtAccessWithScope);
737737
}
738738

739739
public final String getClientId() {
@@ -781,8 +781,8 @@ int getLifetime() {
781781
return lifetime;
782782
}
783783

784-
public boolean getUseJWTAccessWithScope() {
785-
return useJWTAccessWithScope;
784+
public boolean getUseJwtAccessWithScope() {
785+
return useJwtAccessWithScope;
786786
}
787787

788788
@Override
@@ -843,7 +843,7 @@ public int hashCode() {
843843
defaultScopes,
844844
quotaProjectId,
845845
lifetime,
846-
useJWTAccessWithScope);
846+
useJwtAccessWithScope);
847847
}
848848

849849
@Override
@@ -859,7 +859,7 @@ public String toString() {
859859
.add("serviceAccountUser", serviceAccountUser)
860860
.add("quotaProjectId", quotaProjectId)
861861
.add("lifetime", lifetime)
862-
.add("useJWTAccessWithScope", useJWTAccessWithScope)
862+
.add("useJwtAccessWithScope", useJwtAccessWithScope)
863863
.toString();
864864
}
865865

@@ -879,7 +879,7 @@ public boolean equals(Object obj) {
879879
&& Objects.equals(this.defaultScopes, other.defaultScopes)
880880
&& Objects.equals(this.quotaProjectId, other.quotaProjectId)
881881
&& Objects.equals(this.lifetime, other.lifetime)
882-
&& Objects.equals(this.useJWTAccessWithScope, other.useJWTAccessWithScope);
882+
&& Objects.equals(this.useJwtAccessWithScope, other.useJwtAccessWithScope);
883883
}
884884

885885
String createAssertion(JsonFactory jsonFactory, long currentTime, String audience)
@@ -949,19 +949,41 @@ String createAssertionForIdToken(
949949
}
950950
}
951951

952+
/**
953+
* Self signed JWT uses uri as audience, which should have the "https://{host}/" format. For
954+
* instance, if the uri is "https://compute.googleapis.com/compute/v1/projects/", then this
955+
* function returns "https://compute.googleapis.com/".
956+
*/
957+
@VisibleForTesting
958+
static URI getUriForSelfSignedJWT(URI uri) {
959+
if (uri == null || uri.getScheme() == null || uri.getHost() == null) {
960+
return uri;
961+
}
962+
try {
963+
return new URI(uri.getScheme(), uri.getHost(), "/", null);
964+
} catch (URISyntaxException unused) {
965+
return uri;
966+
}
967+
}
968+
952969
@VisibleForTesting
953970
JwtCredentials createSelfSignedJwtCredentials(final URI uri) {
954971
// Create a JwtCredentials for self signed JWT. See https://google.aip.dev/auth/4111.
955972
JwtClaims.Builder claimsBuilder =
956973
JwtClaims.newBuilder().setIssuer(clientEmail).setSubject(clientEmail);
957-
if (!scopes.isEmpty()) {
958-
claimsBuilder.setAdditionalClaims(
959-
Collections.singletonMap("scope", Joiner.on(' ').join(scopes)));
960-
} else if (uri != null) {
961-
claimsBuilder.setAudience(uri.toString());
974+
975+
if (uri == null) {
976+
// If uri is null, use scopes.
977+
String scopeClaim = "";
978+
if (!scopes.isEmpty()) {
979+
scopeClaim = Joiner.on(' ').join(scopes);
980+
} else {
981+
scopeClaim = Joiner.on(' ').join(defaultScopes);
982+
}
983+
claimsBuilder.setAdditionalClaims(Collections.singletonMap("scope", scopeClaim));
962984
} else {
963-
claimsBuilder.setAdditionalClaims(
964-
Collections.singletonMap("scope", Joiner.on(' ').join(defaultScopes)));
985+
// otherwise, use audience with the uri.
986+
claimsBuilder.setAudience(getUriForSelfSignedJWT(uri).toString());
965987
}
966988
return JwtCredentials.newBuilder()
967989
.setPrivateKey(privateKey)
@@ -974,8 +996,10 @@ JwtCredentials createSelfSignedJwtCredentials(final URI uri) {
974996
@Override
975997
public void getRequestMetadata(
976998
final URI uri, Executor executor, final RequestMetadataCallback callback) {
977-
if (useJWTAccessWithScope) {
999+
if (useJwtAccessWithScope) {
9781000
// This will call getRequestMetadata(URI uri), which handles self signed JWT logic.
1001+
// Self signed JWT doesn't use network, so here we do a blocking call to improve
1002+
// efficiency. executor will be ignored since it is intended for async operation.
9791003
blockingGetToCallback(uri, callback);
9801004
} else {
9811005
super.getRequestMetadata(uri, executor, callback);
@@ -985,22 +1009,31 @@ public void getRequestMetadata(
9851009
/** Provide the request metadata by putting an access JWT directly in the metadata. */
9861010
@Override
9871011
public Map<String, List<String>> getRequestMetadata(URI uri) throws IOException {
988-
if (createScopedRequired()) {
989-
if (!useJWTAccessWithScope) {
990-
throw new IOException(
991-
"Scopes are not configured for service account. Specify the scopes"
992-
+ " by calling createScoped or passing scopes to constructor.");
993-
} else if (uri == null) {
994-
throw new IOException("Scopes and uri are not configured for service account.");
995-
}
1012+
if (createScopedRequired() && uri == null) {
1013+
throw new IOException(
1014+
"Scopes and uri are not configured for service account. Specify the scopes"
1015+
+ " by calling createScoped or passing scopes to constructor or"
1016+
+ " providing uri to getRequestMetadata.");
9961017
}
997-
if (useJWTAccessWithScope) {
998-
JwtCredentials jwtCredentials = createSelfSignedJwtCredentials(uri);
999-
Map<String, List<String>> requestMetadata = jwtCredentials.getRequestMetadata(uri);
1000-
return addQuotaProjectIdToRequestMetadata(quotaProjectId, requestMetadata);
1001-
} else {
1018+
1019+
// If scopes are provided but we cannot use self signed JWT, then use scopes in the normal oauth
1020+
// way.
1021+
if (!createScopedRequired() && !useJwtAccessWithScope) {
10021022
return super.getRequestMetadata(uri);
10031023
}
1024+
1025+
// If scopes are provided and self signed JWT can be used, use self signed JWT with scopes.
1026+
// Otherwise, use self signed JWT with uri as the audience.
1027+
JwtCredentials jwtCredentials;
1028+
if (!createScopedRequired() && useJwtAccessWithScope) {
1029+
// Create JWT credentials with the scopes.
1030+
jwtCredentials = createSelfSignedJwtCredentials(null);
1031+
} else {
1032+
// Create JWT credentials with the uri as audience.
1033+
jwtCredentials = createSelfSignedJwtCredentials(uri);
1034+
}
1035+
Map<String, List<String>> requestMetadata = jwtCredentials.getRequestMetadata(null);
1036+
return addQuotaProjectIdToRequestMetadata(quotaProjectId, requestMetadata);
10041037
}
10051038

10061039
@SuppressWarnings("unused")
@@ -1037,7 +1070,7 @@ public static class Builder extends GoogleCredentials.Builder {
10371070
private HttpTransportFactory transportFactory;
10381071
private String quotaProjectId;
10391072
private int lifetime = DEFAULT_LIFETIME_IN_SECONDS;
1040-
private boolean useJWTAccessWithScope = false;
1073+
private boolean useJwtAccessWithScope = false;
10411074

10421075
protected Builder() {}
10431076

@@ -1054,7 +1087,7 @@ protected Builder(ServiceAccountCredentials credentials) {
10541087
this.projectId = credentials.projectId;
10551088
this.quotaProjectId = credentials.quotaProjectId;
10561089
this.lifetime = credentials.lifetime;
1057-
this.useJWTAccessWithScope = credentials.useJWTAccessWithScope;
1090+
this.useJwtAccessWithScope = credentials.useJwtAccessWithScope;
10581091
}
10591092

10601093
public Builder setClientId(String clientId) {
@@ -1119,8 +1152,8 @@ public Builder setLifetime(int lifetime) {
11191152
return this;
11201153
}
11211154

1122-
public Builder setUseJWTAccessWithScope(boolean useJWTAccessWithScope) {
1123-
this.useJWTAccessWithScope = useJWTAccessWithScope;
1155+
public Builder setUseJwtAccessWithScope(boolean useJwtAccessWithScope) {
1156+
this.useJwtAccessWithScope = useJwtAccessWithScope;
11241157
return this;
11251158
}
11261159

@@ -1172,8 +1205,8 @@ public int getLifetime() {
11721205
return lifetime;
11731206
}
11741207

1175-
public boolean getUseJWTAccessWithScope() {
1176-
return useJWTAccessWithScope;
1208+
public boolean getUseJwtAccessWithScope() {
1209+
return useJwtAccessWithScope;
11771210
}
11781211

11791212
public ServiceAccountCredentials build() {
@@ -1190,7 +1223,7 @@ public ServiceAccountCredentials build() {
11901223
projectId,
11911224
quotaProjectId,
11921225
lifetime,
1193-
useJWTAccessWithScope);
1226+
useJwtAccessWithScope);
11941227
}
11951228
}
11961229
}

oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public class ServiceAccountCredentialsTest extends BaseSerializationTest {
106106
private static final String PROJECT_ID = "project-id";
107107
private static final Collection<String> EMPTY_SCOPES = Collections.emptyList();
108108
private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo");
109-
private static final String JWT_AUDIENCE = "http://googleapis.com/testapi/v1/foo";
109+
private static final String JWT_AUDIENCE = "http://googleapis.com/";
110110
private static final HttpTransportFactory DUMMY_TRANSPORT_FACTORY =
111111
new MockTokenServerTransportFactory();
112112
public static final String DEFAULT_ID_TOKEN =
@@ -419,12 +419,12 @@ public void createdScoped_enablesAccessTokens() throws IOException {
419419
null);
420420

421421
try {
422-
credentials.getRequestMetadata(CALL_URI);
422+
credentials.getRequestMetadata(null);
423423
fail("Should not be able to get token without scopes");
424424
} catch (IOException e) {
425425
assertTrue(
426426
"expected to fail with exception",
427-
e.getMessage().contains("Scopes are not configured for service account"));
427+
e.getMessage().contains("Scopes and uri are not configured for service account"));
428428
}
429429

430430
GoogleCredentials scopedCredentials = credentials.createScoped(SCOPES);
@@ -1069,7 +1069,7 @@ public void toString_containsFields() throws IOException {
10691069
String.format(
10701070
"ServiceAccountCredentials{clientId=%s, clientEmail=%s, privateKeyId=%s, "
10711071
+ "transportFactoryClassName=%s, tokenServerUri=%s, scopes=%s, defaultScopes=%s, serviceAccountUser=%s, "
1072-
+ "quotaProjectId=%s, lifetime=3600, useJWTAccessWithScope=false}",
1072+
+ "quotaProjectId=%s, lifetime=3600, useJwtAccessWithScope=false}",
10731073
CLIENT_ID,
10741074
CLIENT_EMAIL,
10751075
PRIVATE_KEY_ID,
@@ -1231,6 +1231,29 @@ public void fromStream_noPrivateKeyId_throws() throws IOException {
12311231
testFromStreamException(serviceAccountStream, "private_key_id");
12321232
}
12331233

1234+
@Test
1235+
public void getUriForSelfSignedJWT() {
1236+
assertNull(ServiceAccountCredentials.getUriForSelfSignedJWT(null));
1237+
1238+
URI uri = URI.create("https://compute.googleapis.com/compute/v1/projects/");
1239+
URI expected = URI.create("https://compute.googleapis.com/");
1240+
assertEquals(expected, ServiceAccountCredentials.getUriForSelfSignedJWT(uri));
1241+
}
1242+
1243+
@Test
1244+
public void getUriForSelfSignedJWT_noHost() {
1245+
URI uri = URI.create("file:foo");
1246+
URI expected = URI.create("file:foo");
1247+
assertEquals(expected, ServiceAccountCredentials.getUriForSelfSignedJWT(uri));
1248+
}
1249+
1250+
@Test
1251+
public void getUriForSelfSignedJWT_forStaticAudience_returnsURI() {
1252+
URI uri = URI.create("compute.googleapis.com");
1253+
URI expected = URI.create("compute.googleapis.com");
1254+
assertEquals(expected, ServiceAccountCredentials.getUriForSelfSignedJWT(uri));
1255+
}
1256+
12341257
@Test
12351258
public void getRequestMetadataSetsQuotaProjectId() throws IOException {
12361259
MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory();
@@ -1335,7 +1358,7 @@ public void getRequestMetadata_selfSignedJWT_withScopes() throws IOException {
13351358
.setServiceAccountUser(USER)
13361359
.setProjectId(PROJECT_ID)
13371360
.setHttpTransportFactory(new MockTokenServerTransportFactory())
1338-
.setUseJWTAccessWithScope(true)
1361+
.setUseJwtAccessWithScope(true)
13391362
.build();
13401363

13411364
Map<String, List<String>> metadata = credentials.getRequestMetadata(CALL_URI);
@@ -1351,11 +1374,9 @@ public void getRequestMetadata_selfSignedJWT_withAudience() throws IOException {
13511374
.setClientEmail(CLIENT_EMAIL)
13521375
.setPrivateKey(privateKey)
13531376
.setPrivateKeyId(PRIVATE_KEY_ID)
1354-
.setScopes(null, SCOPES)
13551377
.setServiceAccountUser(USER)
13561378
.setProjectId(PROJECT_ID)
13571379
.setHttpTransportFactory(new MockTokenServerTransportFactory())
1358-
.setUseJWTAccessWithScope(true)
13591380
.build();
13601381

13611382
Map<String, List<String>> metadata = credentials.getRequestMetadata(CALL_URI);
@@ -1375,7 +1396,7 @@ public void getRequestMetadata_selfSignedJWT_withDefaultScopes() throws IOExcept
13751396
.setServiceAccountUser(USER)
13761397
.setProjectId(PROJECT_ID)
13771398
.setHttpTransportFactory(new MockTokenServerTransportFactory())
1378-
.setUseJWTAccessWithScope(true)
1399+
.setUseJwtAccessWithScope(true)
13791400
.build();
13801401

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

14011423
final AtomicBoolean success = new AtomicBoolean(false);
@@ -1406,7 +1428,7 @@ public void getRequestMetadataWithCallback_selfSignedJWT() throws IOException {
14061428
@Override
14071429
public void onSuccess(Map<String, List<String>> metadata) {
14081430
try {
1409-
verifyJwtAccess(metadata, null);
1431+
verifyJwtAccess(metadata, "dummy.scope");
14101432
} catch (IOException e) {
14111433
fail("Should not throw a failure");
14121434
}

oauth2_http/javatests/com/google/auth/oauth2/functional/FTServiceAccountCredentialsTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@ public final class FTServiceAccountCredentialsTest {
5757
private final String cloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform";
5858

5959
private final String cloudTasksUrl =
60-
"https://cloudtasks.googleapis.com/v2/projects/gcloud-devel/locations";
60+
"https://cloudtasks.googleapis.com/v2/projects/sijunliu-nondca-test/locations";
6161
private final String storageUrl =
62-
"https://storage.googleapis.com/storage/v1/b?project=gcloud-devel";
62+
"https://storage.googleapis.com/storage/v1/b?project=sijunliu-nondca-test";
6363
private final String bigQueryUrl =
64-
"https://bigquery.googleapis.com/bigquery/v2/projects/gcloud-devel/datasets";
64+
"https://bigquery.googleapis.com/bigquery/v2/projects/sijunliu-nondca-test/datasets";
6565
private final String computeUrl =
66-
"https://compute.googleapis.com/compute/v1/projects/gcloud-devel/zones/us-central1-a/instances";
66+
"https://compute.googleapis.com/compute/v1/projects/sijunliu-nondca-test/zones/us-central1-a/instances";
6767

6868
@Test
6969
public void NoScopeNoAudienceComputeTest() throws Exception {

0 commit comments

Comments
 (0)