Skip to content

Commit 256ffef

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 256ffef

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: 49 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,8 @@ 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 The JCA provider to use during request signing. May be null, in which case the default
129+
* provider will be used.
125130
*/
126131
ServiceAccountCredentials(
127132
String clientId,
@@ -133,7 +138,8 @@ public class ServiceAccountCredentials extends GoogleCredentials
133138
URI tokenServerUri,
134139
String serviceAccountUser,
135140
String projectId,
136-
String quotaProjectId) {
141+
String quotaProjectId,
142+
Provider signingProvider) {
137143
this.clientId = clientId;
138144
this.clientEmail = Preconditions.checkNotNull(clientEmail);
139145
this.privateKey = Preconditions.checkNotNull(privateKey);
@@ -143,6 +149,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
143149
firstNonNull(
144150
transportFactory,
145151
getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
152+
this.signingProvider = signingProvider;
146153
this.transportFactoryClassName = this.transportFactory.getClass().getName();
147154
this.tokenServerUri = (tokenServerUri == null) ? OAuth2Utils.TOKEN_SERVER_URI : tokenServerUri;
148155
this.serviceAccountUser = serviceAccountUser;
@@ -324,7 +331,8 @@ static ServiceAccountCredentials fromPkcs8(
324331
tokenServerUri,
325332
serviceAccountUser,
326333
projectId,
327-
quotaProject);
334+
quotaProject,
335+
null);
328336
}
329337

330338
/** Helper to convert from a PKCS#8 String to an RSA private key */
@@ -512,7 +520,8 @@ public GoogleCredentials createScoped(Collection<String> newScopes) {
512520
tokenServerUri,
513521
serviceAccountUser,
514522
projectId,
515-
quotaProjectId);
523+
quotaProjectId,
524+
null);
516525
}
517526

518527
@Override
@@ -527,7 +536,8 @@ public GoogleCredentials createDelegated(String user) {
527536
tokenServerUri,
528537
user,
529538
projectId,
530-
quotaProjectId);
539+
quotaProjectId,
540+
null);
531541
}
532542

533543
public final String getClientId() {
@@ -570,7 +580,9 @@ public String getAccount() {
570580
@Override
571581
public byte[] sign(byte[] toSign) {
572582
try {
573-
Signature signer = Signature.getInstance(OAuth2Utils.SIGNATURE_ALGORITHM);
583+
Signature signer = signingProvider == null
584+
? Signature.getInstance(OAuth2Utils.SIGNATURE_ALGORITHM)
585+
: Signature.getInstance(OAuth2Utils.SIGNATURE_ALGORITHM, signingProvider);
574586
signer.initSign(getPrivateKey());
575587
signer.update(toSign);
576588
return signer.sign();
@@ -647,6 +659,19 @@ public boolean equals(Object obj) {
647659
&& Objects.equals(this.quotaProjectId, other.quotaProjectId);
648660
}
649661

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

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;
695+
String jsonWebSignature = signJsonWebSignature(jsonFactory, header, payload);
696+
return jsonWebSignature;
678697
}
679698

680699
@VisibleForTesting
@@ -698,16 +717,10 @@ String createAssertionForIdToken(
698717
payload.setAudience(audience);
699718
}
700719

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

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-
}
722+
String assertion = signJsonWebSignature(jsonFactory, header, payload);
723+
return assertion;
711724
}
712725

713726
@SuppressWarnings("unused")
@@ -742,6 +755,7 @@ public static class Builder extends GoogleCredentials.Builder {
742755
private Collection<String> scopes;
743756
private HttpTransportFactory transportFactory;
744757
private String quotaProjectId;
758+
private Provider signatureProvider;
745759

746760
protected Builder() {}
747761

@@ -756,6 +770,7 @@ protected Builder(ServiceAccountCredentials credentials) {
756770
this.serviceAccountUser = credentials.serviceAccountUser;
757771
this.projectId = credentials.projectId;
758772
this.quotaProjectId = credentials.quotaProjectId;
773+
this.signatureProvider = credentials.signingProvider;
759774
}
760775

761776
public Builder setClientId(String clientId) {
@@ -808,6 +823,11 @@ public Builder setQuotaProjectId(String quotaProjectId) {
808823
return this;
809824
}
810825

826+
public Builder setSignatureProvider(Provider signatureProvider) {
827+
this.signatureProvider = signatureProvider;
828+
return this;
829+
}
830+
811831
public String getClientId() {
812832
return clientId;
813833
}
@@ -848,6 +868,10 @@ public String getQuotaProjectId() {
848868
return quotaProjectId;
849869
}
850870

871+
public Provider getSignatureProvider() {
872+
return signatureProvider;
873+
}
874+
851875
public ServiceAccountCredentials build() {
852876
return new ServiceAccountCredentials(
853877
clientId,
@@ -859,7 +883,8 @@ public ServiceAccountCredentials build() {
859883
tokenServerUri,
860884
serviceAccountUser,
861885
projectId,
862-
quotaProjectId);
886+
quotaProjectId,
887+
signatureProvider);
863888
}
864889
}
865890
}

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

Lines changed: 40 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,35 @@ 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+
long currentTimeMillis = FixedClock.SYSTEM.currentTimeMillis();
231+
String assertion = credentials.createAssertion(OAuth2Utils.JSON_FACTORY, currentTimeMillis, null);
232+
233+
JsonWebSignature signature = JsonWebSignature.parse(OAuth2Utils.JSON_FACTORY, assertion);
234+
JsonWebToken.Payload payload = signature.getPayload();
235+
assertEquals(CLIENT_EMAIL, payload.getIssuer());
236+
assertEquals(OAuth2Utils.TOKEN_SERVER_URI.toString(), payload.getAudience());
237+
assertEquals(currentTimeMillis / 1000, (long) payload.getIssuedAtTimeSeconds());
238+
assertEquals(currentTimeMillis / 1000 + 3600, (long) payload.getExpirationTimeSeconds());
239+
assertEquals(USER, payload.getSubject());
240+
assertEquals(Joiner.on(' ').join(scopes), payload.get("scope"));
241+
}
242+
203243
@Test
204244
public void createAssertionForIdToken_correct() throws IOException {
205245

0 commit comments

Comments
 (0)