diff --git a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java index f26448545..3fedeaccc 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java @@ -46,6 +46,7 @@ import com.google.api.client.json.JsonObjectParser; import com.google.api.client.json.webtoken.JsonWebSignature; import com.google.api.client.json.webtoken.JsonWebToken; +import com.google.api.client.util.Base64; import com.google.api.client.util.ExponentialBackOff; import com.google.api.client.util.GenericData; import com.google.api.client.util.Joiner; @@ -53,6 +54,7 @@ import com.google.api.client.util.PemReader.Section; import com.google.api.client.util.Preconditions; import com.google.api.client.util.SecurityUtils; +import com.google.api.client.util.StringUtils; import com.google.auth.ServiceAccountSigner; import com.google.auth.http.HttpTransportFactory; import com.google.common.annotations.Beta; @@ -66,11 +68,11 @@ import java.io.StringReader; import java.net.URI; import java.net.URISyntaxException; -import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; +import java.security.Provider; import java.security.Signature; import java.security.SignatureException; import java.security.spec.InvalidKeySpecException; @@ -103,6 +105,7 @@ public class ServiceAccountCredentials extends GoogleCredentials private final URI tokenServerUri; private final Collection scopes; private final String quotaProjectId; + private final Provider signingProvider; private transient HttpTransportFactory transportFactory; @@ -122,6 +125,7 @@ public class ServiceAccountCredentials extends GoogleCredentials * authority to the service account. * @param projectId the project used for billing * @param quotaProjectId The project used for quota and billing purposes. May be null. + * @param signingProvider Explicitly set the JCA provider to use during request signing. May be null. */ ServiceAccountCredentials( String clientId, @@ -133,7 +137,8 @@ public class ServiceAccountCredentials extends GoogleCredentials URI tokenServerUri, String serviceAccountUser, String projectId, - String quotaProjectId) { + String quotaProjectId, + Provider signingProvider) { this.clientId = clientId; this.clientEmail = Preconditions.checkNotNull(clientEmail); this.privateKey = Preconditions.checkNotNull(privateKey); @@ -143,6 +148,7 @@ public class ServiceAccountCredentials extends GoogleCredentials firstNonNull( transportFactory, getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY)); + this.signingProvider = signingProvider; this.transportFactoryClassName = this.transportFactory.getClass().getName(); this.tokenServerUri = (tokenServerUri == null) ? OAuth2Utils.TOKEN_SERVER_URI : tokenServerUri; this.serviceAccountUser = serviceAccountUser; @@ -324,7 +330,8 @@ static ServiceAccountCredentials fromPkcs8( tokenServerUri, serviceAccountUser, projectId, - quotaProject); + quotaProject, + null); } /** Helper to convert from a PKCS#8 String to an RSA private key */ @@ -512,7 +519,8 @@ public GoogleCredentials createScoped(Collection newScopes) { tokenServerUri, serviceAccountUser, projectId, - quotaProjectId); + quotaProjectId, + null); } @Override @@ -527,7 +535,8 @@ public GoogleCredentials createDelegated(String user) { tokenServerUri, user, projectId, - quotaProjectId); + quotaProjectId, + null); } public final String getClientId() { @@ -570,7 +579,9 @@ public String getAccount() { @Override public byte[] sign(byte[] toSign) { try { - Signature signer = Signature.getInstance(OAuth2Utils.SIGNATURE_ALGORITHM); + Signature signer = signingProvider == null + ? Signature.getInstance(OAuth2Utils.SIGNATURE_ALGORITHM) + : Signature.getInstance(OAuth2Utils.SIGNATURE_ALGORITHM, signingProvider); signer.initSign(getPrivateKey()); signer.update(toSign); return signer.sign(); @@ -647,6 +658,19 @@ public boolean equals(Object obj) { && Objects.equals(this.quotaProjectId, other.quotaProjectId); } + private String signJsonWebSignature(JsonFactory jsonFactory, JsonWebSignature.Header header, JsonWebToken.Payload payload) throws IOException { + String signedContentString = Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(header)) + "." + Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(payload)); + byte[] signedContentBytes = StringUtils.getBytesUtf8(signedContentString); + try { + byte[] signature = this.sign(signedContentBytes); + return signedContentString + "." + Base64.encodeBase64URLSafeString(signature); + + } catch (Exception e) { + throw new IOException( + "Error signing service account access token request with private key.", e); + } + } + String createAssertion(JsonFactory jsonFactory, long currentTime, String audience) throws IOException { JsonWebSignature.Header header = new JsonWebSignature.Header(); @@ -667,14 +691,8 @@ String createAssertion(JsonFactory jsonFactory, long currentTime, String audienc payload.setAudience(audience); } - String assertion; - try { - assertion = JsonWebSignature.signUsingRsaSha256(privateKey, jsonFactory, header, payload); - } catch (GeneralSecurityException e) { - throw new IOException( - "Error signing service account access token request with private key.", e); - } - return assertion; + String jsonWebSignature = signJsonWebSignature(jsonFactory, header, payload); + return jsonWebSignature; } @VisibleForTesting @@ -698,16 +716,10 @@ String createAssertionForIdToken( payload.setAudience(audience); } - try { - payload.set("target_audience", targetAudience); + payload.set("target_audience", targetAudience); - String assertion = - JsonWebSignature.signUsingRsaSha256(privateKey, jsonFactory, header, payload); - return assertion; - } catch (GeneralSecurityException e) { - throw new IOException( - "Error signing service account access token request with private key.", e); - } + String assertion = signJsonWebSignature(jsonFactory, header, payload); + return assertion; } @SuppressWarnings("unused") @@ -742,6 +754,7 @@ public static class Builder extends GoogleCredentials.Builder { private Collection scopes; private HttpTransportFactory transportFactory; private String quotaProjectId; + private Provider signatureProvider; protected Builder() {} @@ -756,6 +769,7 @@ protected Builder(ServiceAccountCredentials credentials) { this.serviceAccountUser = credentials.serviceAccountUser; this.projectId = credentials.projectId; this.quotaProjectId = credentials.quotaProjectId; + this.signatureProvider = credentials.signingProvider; } public Builder setClientId(String clientId) { @@ -808,6 +822,11 @@ public Builder setQuotaProjectId(String quotaProjectId) { return this; } + public Builder setSignatureProvider(Provider signatureProvider) { + this.signatureProvider = signatureProvider; + return this; + } + public String getClientId() { return clientId; } @@ -848,6 +867,10 @@ public String getQuotaProjectId() { return quotaProjectId; } + public Provider getSignatureProvider() { + return signatureProvider; + } + public ServiceAccountCredentials build() { return new ServiceAccountCredentials( clientId, @@ -859,7 +882,8 @@ public ServiceAccountCredentials build() { tokenServerUri, serviceAccountUser, projectId, - quotaProjectId); + quotaProjectId, + signatureProvider); } } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java index 779c7f006..4e0833645 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java @@ -49,6 +49,7 @@ import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.api.client.util.Clock; import com.google.api.client.util.Joiner; +import com.google.api.client.util.SecurityUtils; import com.google.auth.TestUtils; import com.google.auth.http.HttpTransportFactory; import com.google.auth.oauth2.GoogleCredentialsTest.MockHttpTransportFactory; @@ -61,6 +62,7 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; +import java.security.Provider; import java.security.Signature; import java.security.SignatureException; import java.util.Arrays; @@ -109,6 +111,15 @@ public class ServiceAccountCredentialsTest extends BaseSerializationTest { + "aXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAyMTAxNTUwODM0MjAwNzA4NTY4In0" + ".redacted"; private static final String QUOTA_PROJECT = "sample-quota-project-id"; + private static Provider defaultRsaSignatureProvider; + + static { + try { + defaultRsaSignatureProvider = SecurityUtils.getRsaKeyFactory().getProvider(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("RSA keys not supported on this JVM", e); + } + } @Test public void createdScoped_clones() throws IOException { @@ -200,6 +211,36 @@ public void createAssertion_correct() throws IOException { assertEquals(Joiner.on(' ').join(scopes), payload.get("scope")); } + @Test + public void createAssertionWithExplicitProvider_correct() throws IOException { + PrivateKey privateKey = ServiceAccountCredentials.privateKeyFromPkcs8(PRIVATE_KEY_PKCS8); + List scopes = Arrays.asList("scope1", "scope2"); + ServiceAccountCredentials credentials = + ServiceAccountCredentials.newBuilder() + .setClientId(CLIENT_ID) + .setClientEmail(CLIENT_EMAIL) + .setPrivateKey(privateKey) + .setPrivateKeyId(PRIVATE_KEY_ID) + .setScopes(scopes) + .setServiceAccountUser(USER) + .setProjectId(PROJECT_ID) + .setSignatureProvider(defaultRsaSignatureProvider) + .build(); + + JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY; + long currentTimeMillis = Clock.SYSTEM.currentTimeMillis(); + String assertion = credentials.createAssertion(jsonFactory, currentTimeMillis, null); + + JsonWebSignature signature = JsonWebSignature.parse(jsonFactory, assertion); + JsonWebToken.Payload payload = signature.getPayload(); + assertEquals(CLIENT_EMAIL, payload.getIssuer()); + assertEquals(OAuth2Utils.TOKEN_SERVER_URI.toString(), payload.getAudience()); + assertEquals(currentTimeMillis / 1000, (long) payload.getIssuedAtTimeSeconds()); + assertEquals(currentTimeMillis / 1000 + 3600, (long) payload.getExpirationTimeSeconds()); + assertEquals(USER, payload.getSubject()); + assertEquals(Joiner.on(' ').join(scopes), payload.get("scope")); + } + @Test public void createAssertionForIdToken_correct() throws IOException {