Skip to content

Commit 7054a1a

Browse files
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 7054a1a

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)