Skip to content

Commit 2eb46f9

Browse files
feat: Add support for explicit java.security.Provider to ServiceAccountCredentials
This enables the storage of service account credentials in external key storage such as remote KeyVaults or HSMs.
1 parent 0a57cd5 commit 2eb46f9

File tree

2 files changed

+89
-24
lines changed

2 files changed

+89
-24
lines changed

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

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,15 @@
4646
import com.google.api.client.json.JsonObjectParser;
4747
import com.google.api.client.json.webtoken.JsonWebSignature;
4848
import com.google.api.client.json.webtoken.JsonWebToken;
49+
import com.google.api.client.util.Base64;
4950
import com.google.api.client.util.ExponentialBackOff;
5051
import com.google.api.client.util.GenericData;
5152
import com.google.api.client.util.Joiner;
5253
import com.google.api.client.util.PemReader;
5354
import com.google.api.client.util.PemReader.Section;
5455
import com.google.api.client.util.Preconditions;
5556
import com.google.api.client.util.SecurityUtils;
57+
import com.google.api.client.util.StringUtils;
5658
import com.google.auth.ServiceAccountSigner;
5759
import com.google.auth.http.HttpTransportFactory;
5860
import com.google.common.annotations.Beta;
@@ -66,11 +68,11 @@
6668
import java.io.StringReader;
6769
import java.net.URI;
6870
import java.net.URISyntaxException;
69-
import java.security.GeneralSecurityException;
7071
import java.security.InvalidKeyException;
7172
import java.security.KeyFactory;
7273
import java.security.NoSuchAlgorithmException;
7374
import java.security.PrivateKey;
75+
import java.security.Provider;
7476
import java.security.Signature;
7577
import java.security.SignatureException;
7678
import java.security.spec.InvalidKeySpecException;
@@ -103,6 +105,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
103105
private final URI tokenServerUri;
104106
private final Collection<String> scopes;
105107
private final String quotaProjectId;
108+
private final Provider signingProvider;
106109

107110
private transient HttpTransportFactory transportFactory;
108111

@@ -122,6 +125,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
122125
* authority to the service account.
123126
* @param projectId the project used for billing
124127
* @param quotaProjectId The project used for quota and billing purposes. May be null.
128+
* @param signingProvider Explicitly set the JCA provider to use during request signing. May be null.
125129
*/
126130
ServiceAccountCredentials(
127131
String clientId,
@@ -133,7 +137,8 @@ public class ServiceAccountCredentials extends GoogleCredentials
133137
URI tokenServerUri,
134138
String serviceAccountUser,
135139
String projectId,
136-
String quotaProjectId) {
140+
String quotaProjectId,
141+
Provider signingProvider) {
137142
this.clientId = clientId;
138143
this.clientEmail = Preconditions.checkNotNull(clientEmail);
139144
this.privateKey = Preconditions.checkNotNull(privateKey);
@@ -143,6 +148,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
143148
firstNonNull(
144149
transportFactory,
145150
getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
151+
this.signingProvider = signingProvider;
146152
this.transportFactoryClassName = this.transportFactory.getClass().getName();
147153
this.tokenServerUri = (tokenServerUri == null) ? OAuth2Utils.TOKEN_SERVER_URI : tokenServerUri;
148154
this.serviceAccountUser = serviceAccountUser;
@@ -324,7 +330,8 @@ static ServiceAccountCredentials fromPkcs8(
324330
tokenServerUri,
325331
serviceAccountUser,
326332
projectId,
327-
quotaProject);
333+
quotaProject,
334+
null);
328335
}
329336

330337
/** Helper to convert from a PKCS#8 String to an RSA private key */
@@ -512,7 +519,8 @@ public GoogleCredentials createScoped(Collection<String> newScopes) {
512519
tokenServerUri,
513520
serviceAccountUser,
514521
projectId,
515-
quotaProjectId);
522+
quotaProjectId,
523+
null);
516524
}
517525

518526
@Override
@@ -527,7 +535,8 @@ public GoogleCredentials createDelegated(String user) {
527535
tokenServerUri,
528536
user,
529537
projectId,
530-
quotaProjectId);
538+
quotaProjectId,
539+
null);
531540
}
532541

533542
public final String getClientId() {
@@ -570,7 +579,9 @@ public String getAccount() {
570579
@Override
571580
public byte[] sign(byte[] toSign) {
572581
try {
573-
Signature signer = Signature.getInstance(OAuth2Utils.SIGNATURE_ALGORITHM);
582+
Signature signer = signingProvider == null
583+
? Signature.getInstance(OAuth2Utils.SIGNATURE_ALGORITHM)
584+
: Signature.getInstance(OAuth2Utils.SIGNATURE_ALGORITHM, signingProvider);
574585
signer.initSign(getPrivateKey());
575586
signer.update(toSign);
576587
return signer.sign();
@@ -647,6 +658,19 @@ public boolean equals(Object obj) {
647658
&& Objects.equals(this.quotaProjectId, other.quotaProjectId);
648659
}
649660

661+
private String signJsonWebSignature(JsonFactory jsonFactory, JsonWebSignature.Header header, JsonWebToken.Payload payload) throws IOException {
662+
String signedContentString = Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(header)) + "." + Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(payload));
663+
byte[] signedContentBytes = StringUtils.getBytesUtf8(signedContentString);
664+
try {
665+
byte[] signature = this.sign(signedContentBytes);
666+
return signedContentString + "." + Base64.encodeBase64URLSafeString(signature);
667+
668+
} catch (Exception e) {
669+
throw new IOException(
670+
"Error signing service account access token request with private key.", e);
671+
}
672+
}
673+
650674
String createAssertion(JsonFactory jsonFactory, long currentTime, String audience)
651675
throws IOException {
652676
JsonWebSignature.Header header = new JsonWebSignature.Header();
@@ -667,14 +691,8 @@ String createAssertion(JsonFactory jsonFactory, long currentTime, String audienc
667691
payload.setAudience(audience);
668692
}
669693

670-
String assertion;
671-
try {
672-
assertion = JsonWebSignature.signUsingRsaSha256(privateKey, jsonFactory, header, payload);
673-
} catch (GeneralSecurityException e) {
674-
throw new IOException(
675-
"Error signing service account access token request with private key.", e);
676-
}
677-
return assertion;
694+
String jsonWebSignature = signJsonWebSignature(jsonFactory, header, payload);
695+
return jsonWebSignature;
678696
}
679697

680698
@VisibleForTesting
@@ -698,16 +716,10 @@ String createAssertionForIdToken(
698716
payload.setAudience(audience);
699717
}
700718

701-
try {
702-
payload.set("target_audience", targetAudience);
719+
payload.set("target_audience", targetAudience);
703720

704-
String assertion =
705-
JsonWebSignature.signUsingRsaSha256(privateKey, jsonFactory, header, payload);
706-
return assertion;
707-
} catch (GeneralSecurityException e) {
708-
throw new IOException(
709-
"Error signing service account access token request with private key.", e);
710-
}
721+
String assertion = signJsonWebSignature(jsonFactory, header, payload);
722+
return assertion;
711723
}
712724

713725
@SuppressWarnings("unused")
@@ -742,6 +754,7 @@ public static class Builder extends GoogleCredentials.Builder {
742754
private Collection<String> scopes;
743755
private HttpTransportFactory transportFactory;
744756
private String quotaProjectId;
757+
private Provider signatureProvider;
745758

746759
protected Builder() {}
747760

@@ -756,6 +769,7 @@ protected Builder(ServiceAccountCredentials credentials) {
756769
this.serviceAccountUser = credentials.serviceAccountUser;
757770
this.projectId = credentials.projectId;
758771
this.quotaProjectId = credentials.quotaProjectId;
772+
this.signatureProvider = credentials.signingProvider;
759773
}
760774

761775
public Builder setClientId(String clientId) {
@@ -808,6 +822,11 @@ public Builder setQuotaProjectId(String quotaProjectId) {
808822
return this;
809823
}
810824

825+
public Builder setSignatureProvider(Provider signatureProvider) {
826+
this.signatureProvider = signatureProvider;
827+
return this;
828+
}
829+
811830
public String getClientId() {
812831
return clientId;
813832
}
@@ -848,6 +867,10 @@ public String getQuotaProjectId() {
848867
return quotaProjectId;
849868
}
850869

870+
public Provider getSignatureProvider() {
871+
return signatureProvider;
872+
}
873+
851874
public ServiceAccountCredentials build() {
852875
return new ServiceAccountCredentials(
853876
clientId,
@@ -859,7 +882,8 @@ public ServiceAccountCredentials build() {
859882
tokenServerUri,
860883
serviceAccountUser,
861884
projectId,
862-
quotaProjectId);
885+
quotaProjectId,
886+
signatureProvider);
863887
}
864888
}
865889
}

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
5050
import com.google.api.client.util.Clock;
5151
import com.google.api.client.util.Joiner;
52+
import com.google.api.client.util.SecurityUtils;
5253
import com.google.auth.TestUtils;
5354
import com.google.auth.http.HttpTransportFactory;
5455
import com.google.auth.oauth2.GoogleCredentialsTest.MockHttpTransportFactory;
@@ -61,6 +62,7 @@
6162
import java.security.InvalidKeyException;
6263
import java.security.NoSuchAlgorithmException;
6364
import java.security.PrivateKey;
65+
import java.security.Provider;
6466
import java.security.Signature;
6567
import java.security.SignatureException;
6668
import java.util.Arrays;
@@ -109,6 +111,15 @@ public class ServiceAccountCredentialsTest extends BaseSerializationTest {
109111
+ "aXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAyMTAxNTUwODM0MjAwNzA4NTY4In0"
110112
+ ".redacted";
111113
private static final String QUOTA_PROJECT = "sample-quota-project-id";
114+
private static Provider defaultRsaSignatureProvider;
115+
116+
static {
117+
try {
118+
defaultRsaSignatureProvider = SecurityUtils.getRsaKeyFactory().getProvider();
119+
} catch (NoSuchAlgorithmException e) {
120+
throw new RuntimeException("RSA keys not supported on this JVM", e);
121+
}
122+
}
112123

113124
@Test
114125
public void createdScoped_clones() throws IOException {
@@ -200,6 +211,36 @@ public void createAssertion_correct() throws IOException {
200211
assertEquals(Joiner.on(' ').join(scopes), payload.get("scope"));
201212
}
202213

214+
@Test
215+
public void createAssertionWithExplicitProvider_correct() throws IOException {
216+
PrivateKey privateKey = ServiceAccountCredentials.privateKeyFromPkcs8(PRIVATE_KEY_PKCS8);
217+
List<String> scopes = Arrays.asList("scope1", "scope2");
218+
ServiceAccountCredentials credentials =
219+
ServiceAccountCredentials.newBuilder()
220+
.setClientId(CLIENT_ID)
221+
.setClientEmail(CLIENT_EMAIL)
222+
.setPrivateKey(privateKey)
223+
.setPrivateKeyId(PRIVATE_KEY_ID)
224+
.setScopes(scopes)
225+
.setServiceAccountUser(USER)
226+
.setProjectId(PROJECT_ID)
227+
.setSignatureProvider(defaultRsaSignatureProvider)
228+
.build();
229+
230+
JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY;
231+
long currentTimeMillis = Clock.SYSTEM.currentTimeMillis();
232+
String assertion = credentials.createAssertion(jsonFactory, currentTimeMillis, null);
233+
234+
JsonWebSignature signature = JsonWebSignature.parse(jsonFactory, assertion);
235+
JsonWebToken.Payload payload = signature.getPayload();
236+
assertEquals(CLIENT_EMAIL, payload.getIssuer());
237+
assertEquals(OAuth2Utils.TOKEN_SERVER_URI.toString(), payload.getAudience());
238+
assertEquals(currentTimeMillis / 1000, (long) payload.getIssuedAtTimeSeconds());
239+
assertEquals(currentTimeMillis / 1000 + 3600, (long) payload.getExpirationTimeSeconds());
240+
assertEquals(USER, payload.getSubject());
241+
assertEquals(Joiner.on(' ').join(scopes), payload.get("scope"));
242+
}
243+
203244
@Test
204245
public void createAssertionForIdToken_correct() throws IOException {
205246

0 commit comments

Comments
 (0)