7777import java .security .spec .InvalidKeySpecException ;
7878import java .security .spec .PKCS8EncodedKeySpec ;
7979import java .util .Collection ;
80+ import java .util .Collections ;
8081import java .util .Date ;
8182import java .util .List ;
8283import java .util .Map ;
@@ -109,9 +110,9 @@ public class ServiceAccountCredentials extends GoogleCredentials
109110 private final Collection <String > defaultScopes ;
110111 private final String quotaProjectId ;
111112 private final int lifetime ;
113+ private final boolean useJwtAccessWithScope ;
112114
113115 private transient HttpTransportFactory transportFactory ;
114- private transient ServiceAccountJwtAccessCredentials jwtCredentials = null ;
115116
116117 /**
117118 * Constructor with minimum identifying information and custom HTTP transport.
@@ -133,6 +134,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
133134 * most 43200 (12 hours). If the token is used for calling a Google API, then the value should
134135 * be at most 3600 (1 hour). If the given value is 0, then the default value 3600 will be used
135136 * when creating the credentials.
137+ * @param useJwtAccessWithScope whether self signed JWT with scopes should be always used.
136138 */
137139 ServiceAccountCredentials (
138140 String clientId ,
@@ -146,7 +148,8 @@ public class ServiceAccountCredentials extends GoogleCredentials
146148 String serviceAccountUser ,
147149 String projectId ,
148150 String quotaProjectId ,
149- int lifetime ) {
151+ int lifetime ,
152+ boolean useJwtAccessWithScope ) {
150153 this .clientId = clientId ;
151154 this .clientEmail = Preconditions .checkNotNull (clientEmail );
152155 this .privateKey = Preconditions .checkNotNull (privateKey );
@@ -167,18 +170,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
167170 throw new IllegalStateException ("lifetime must be less than or equal to 43200" );
168171 }
169172 this .lifetime = lifetime ;
170-
171- // Use self signed JWT if scopes is not set, see https://google.aip.dev/auth/4111.
172- if (this .scopes .isEmpty ()) {
173- jwtCredentials =
174- new ServiceAccountJwtAccessCredentials .Builder ()
175- .setClientEmail (clientEmail )
176- .setClientId (clientId )
177- .setPrivateKey (privateKey )
178- .setPrivateKeyId (privateKeyId )
179- .setQuotaProjectId (quotaProjectId )
180- .build ();
181- }
173+ this .useJwtAccessWithScope = useJwtAccessWithScope ;
182174 }
183175
184176 /**
@@ -492,7 +484,8 @@ static ServiceAccountCredentials fromPkcs8(
492484 serviceAccountUser ,
493485 projectId ,
494486 quotaProject ,
495- DEFAULT_LIFETIME_IN_SECONDS );
487+ DEFAULT_LIFETIME_IN_SECONDS ,
488+ false );
496489 }
497490
498491 /** Helper to convert from a PKCS#8 String to an RSA private key */
@@ -698,7 +691,8 @@ public GoogleCredentials createScoped(
698691 serviceAccountUser ,
699692 projectId ,
700693 quotaProjectId ,
701- lifetime );
694+ lifetime ,
695+ useJwtAccessWithScope );
702696 }
703697
704698 /**
@@ -714,6 +708,16 @@ public ServiceAccountCredentials createWithCustomLifetime(int lifetime) {
714708 return this .toBuilder ().setLifetime (lifetime ).build ();
715709 }
716710
711+ /**
712+ * Clones the service account with a new useJwtAccessWithScope value.
713+ *
714+ * @param useJwtAccessWithScope whether self signed JWT with scopes should be used
715+ * @return the cloned service account credentials with the given useJwtAccessWithScope
716+ */
717+ public ServiceAccountCredentials createWithUseJwtAccessWithScope (boolean useJwtAccessWithScope ) {
718+ return this .toBuilder ().setUseJwtAccessWithScope (useJwtAccessWithScope ).build ();
719+ }
720+
717721 @ Override
718722 public GoogleCredentials createDelegated (String user ) {
719723 return new ServiceAccountCredentials (
@@ -728,7 +732,8 @@ public GoogleCredentials createDelegated(String user) {
728732 user ,
729733 projectId ,
730734 quotaProjectId ,
731- lifetime );
735+ lifetime ,
736+ useJwtAccessWithScope );
732737 }
733738
734739 public final String getClientId () {
@@ -776,6 +781,10 @@ int getLifetime() {
776781 return lifetime ;
777782 }
778783
784+ public boolean getUseJwtAccessWithScope () {
785+ return useJwtAccessWithScope ;
786+ }
787+
779788 @ Override
780789 public String getAccount () {
781790 return getClientEmail ();
@@ -833,7 +842,8 @@ public int hashCode() {
833842 scopes ,
834843 defaultScopes ,
835844 quotaProjectId ,
836- lifetime );
845+ lifetime ,
846+ useJwtAccessWithScope );
837847 }
838848
839849 @ Override
@@ -849,6 +859,7 @@ public String toString() {
849859 .add ("serviceAccountUser" , serviceAccountUser )
850860 .add ("quotaProjectId" , quotaProjectId )
851861 .add ("lifetime" , lifetime )
862+ .add ("useJwtAccessWithScope" , useJwtAccessWithScope )
852863 .toString ();
853864 }
854865
@@ -867,7 +878,8 @@ public boolean equals(Object obj) {
867878 && Objects .equals (this .scopes , other .scopes )
868879 && Objects .equals (this .defaultScopes , other .defaultScopes )
869880 && Objects .equals (this .quotaProjectId , other .quotaProjectId )
870- && Objects .equals (this .lifetime , other .lifetime );
881+ && Objects .equals (this .lifetime , other .lifetime )
882+ && Objects .equals (this .useJwtAccessWithScope , other .useJwtAccessWithScope );
871883 }
872884
873885 String createAssertion (JsonFactory jsonFactory , long currentTime , String audience )
@@ -937,11 +949,58 @@ String createAssertionForIdToken(
937949 }
938950 }
939951
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+
969+ @ VisibleForTesting
970+ JwtCredentials createSelfSignedJwtCredentials (final URI uri ) {
971+ // Create a JwtCredentials for self signed JWT. See https://google.aip.dev/auth/4111.
972+ JwtClaims .Builder claimsBuilder =
973+ JwtClaims .newBuilder ().setIssuer (clientEmail ).setSubject (clientEmail );
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 ));
984+ } else {
985+ // otherwise, use audience with the uri.
986+ claimsBuilder .setAudience (getUriForSelfSignedJWT (uri ).toString ());
987+ }
988+ return JwtCredentials .newBuilder ()
989+ .setPrivateKey (privateKey )
990+ .setPrivateKeyId (privateKeyId )
991+ .setJwtClaims (claimsBuilder .build ())
992+ .setClock (clock )
993+ .build ();
994+ }
995+
940996 @ Override
941997 public void getRequestMetadata (
942998 final URI uri , Executor executor , final RequestMetadataCallback callback ) {
943- if (jwtCredentials != null && uri != null ) {
944- jwtCredentials .getRequestMetadata (uri , executor , callback );
999+ if (useJwtAccessWithScope ) {
1000+ // 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.
1003+ blockingGetToCallback (uri , callback );
9451004 } else {
9461005 super .getRequestMetadata (uri , executor , callback );
9471006 }
@@ -950,17 +1009,31 @@ public void getRequestMetadata(
9501009 /** Provide the request metadata by putting an access JWT directly in the metadata. */
9511010 @ Override
9521011 public Map <String , List <String >> getRequestMetadata (URI uri ) throws IOException {
953- if (scopes . isEmpty () && defaultScopes . isEmpty () && uri == null ) {
1012+ if (createScopedRequired () && uri == null ) {
9541013 throw new IOException (
955- "Scopes and uri are not configured for service account. Either pass uri "
956- + " to getRequestMetadata to use self signed JWT, or specify the scopes "
957- + " by calling createScoped or passing scopes to constructor ." );
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 ." );
9581017 }
959- if (jwtCredentials != null && uri != null ) {
960- return jwtCredentials .getRequestMetadata (uri );
961- } else {
1018+
1019+ // If scopes are provided but we cannot use self signed JWT, then use scopes to get access
1020+ // token.
1021+ if (!createScopedRequired () && !useJwtAccessWithScope ) {
9621022 return super .getRequestMetadata (uri );
9631023 }
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 );
9641037 }
9651038
9661039 @ SuppressWarnings ("unused" )
@@ -997,6 +1070,7 @@ public static class Builder extends GoogleCredentials.Builder {
9971070 private HttpTransportFactory transportFactory ;
9981071 private String quotaProjectId ;
9991072 private int lifetime = DEFAULT_LIFETIME_IN_SECONDS ;
1073+ private boolean useJwtAccessWithScope = false ;
10001074
10011075 protected Builder () {}
10021076
@@ -1013,6 +1087,7 @@ protected Builder(ServiceAccountCredentials credentials) {
10131087 this .projectId = credentials .projectId ;
10141088 this .quotaProjectId = credentials .quotaProjectId ;
10151089 this .lifetime = credentials .lifetime ;
1090+ this .useJwtAccessWithScope = credentials .useJwtAccessWithScope ;
10161091 }
10171092
10181093 public Builder setClientId (String clientId ) {
@@ -1077,6 +1152,11 @@ public Builder setLifetime(int lifetime) {
10771152 return this ;
10781153 }
10791154
1155+ public Builder setUseJwtAccessWithScope (boolean useJwtAccessWithScope ) {
1156+ this .useJwtAccessWithScope = useJwtAccessWithScope ;
1157+ return this ;
1158+ }
1159+
10801160 public String getClientId () {
10811161 return clientId ;
10821162 }
@@ -1125,6 +1205,10 @@ public int getLifetime() {
11251205 return lifetime ;
11261206 }
11271207
1208+ public boolean getUseJwtAccessWithScope () {
1209+ return useJwtAccessWithScope ;
1210+ }
1211+
11281212 public ServiceAccountCredentials build () {
11291213 return new ServiceAccountCredentials (
11301214 clientId ,
@@ -1138,7 +1222,8 @@ public ServiceAccountCredentials build() {
11381222 serviceAccountUser ,
11391223 projectId ,
11401224 quotaProjectId ,
1141- lifetime );
1225+ lifetime ,
1226+ useJwtAccessWithScope );
11421227 }
11431228 }
11441229}
0 commit comments