diff --git a/src/main/java/com/google/firebase/auth/AuthErrorHandler.java b/src/main/java/com/google/firebase/auth/AuthErrorHandler.java index a6cea3914..6ed3f50ce 100644 --- a/src/main/java/com/google/firebase/auth/AuthErrorHandler.java +++ b/src/main/java/com/google/firebase/auth/AuthErrorHandler.java @@ -99,12 +99,7 @@ protected FirebaseAuthException createException(FirebaseException base) { errorInfo.getAuthErrorCode()); } - return new FirebaseAuthException( - base.getErrorCodeNew(), - base.getMessage(), - base.getCause(), - base.getHttpResponse(), - null); + return new FirebaseAuthException(base); } private String getResponse(FirebaseException base) { diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index f7f6231ad..a7c36039f 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -41,7 +41,6 @@ import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; -import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @@ -58,8 +57,6 @@ public class FirebaseAuth { private static final String SERVICE_ID = FirebaseAuth.class.getName(); - private static final String ERROR_CUSTOM_TOKEN = "ERROR_CUSTOM_TOKEN"; - private final Object lock = new Object(); private final AtomicBoolean destroyed = new AtomicBoolean(false); @@ -331,12 +328,7 @@ private CallableOperation createCustomTokenOp( return new CallableOperation() { @Override public String execute() throws FirebaseAuthException { - try { - return tokenFactory.createSignedCustomAuthTokenForUser(uid, developerClaims); - } catch (IOException e) { - throw new FirebaseAuthException(ERROR_CUSTOM_TOKEN, - "Failed to generate a custom token", e); - } + return tokenFactory.createSignedCustomAuthTokenForUser(uid, developerClaims); } }; } diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuthException.java b/src/main/java/com/google/firebase/auth/FirebaseAuthException.java index f50bc1ff0..b1f84df6e 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuthException.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuthException.java @@ -16,9 +16,6 @@ package com.google.firebase.auth; -import static com.google.common.base.Preconditions.checkArgument; - -import com.google.common.base.Strings; import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseException; import com.google.firebase.IncomingHttpResponse; @@ -33,7 +30,7 @@ public class FirebaseAuthException extends FirebaseException { private final AuthErrorCode errorCode; - FirebaseAuthException( + public FirebaseAuthException( @NonNull ErrorCode errorCode, @NonNull String message, Throwable cause, @@ -43,12 +40,13 @@ public class FirebaseAuthException extends FirebaseException { this.errorCode = authErrorCode; } - @Deprecated public FirebaseAuthException( - @NonNull String errorCode, @NonNull String detailMessage, Throwable throwable) { - super(detailMessage, throwable); - checkArgument(!Strings.isNullOrEmpty(errorCode)); - this.errorCode = null; + @NonNull ErrorCode errorCode, @NonNull String message, Throwable throwable) { + this(errorCode, message, throwable, null, null); + } + + public FirebaseAuthException(FirebaseException base) { + this(base.getErrorCodeNew(), base.getMessage(), base.getCause(), base.getHttpResponse(), null); } @Nullable diff --git a/src/main/java/com/google/firebase/auth/internal/CryptoSigner.java b/src/main/java/com/google/firebase/auth/internal/CryptoSigner.java index 3036f9f28..2ff30a20c 100644 --- a/src/main/java/com/google/firebase/auth/internal/CryptoSigner.java +++ b/src/main/java/com/google/firebase/auth/internal/CryptoSigner.java @@ -16,6 +16,7 @@ package com.google.firebase.auth.internal; +import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.internal.NonNull; import java.io.IOException; @@ -32,10 +33,10 @@ interface CryptoSigner { * * @param payload Data to be signed * @return Signature as a byte array - * @throws IOException If an error occurs during signing + * @throws FirebaseAuthException If an error occurs during signing */ @NonNull - byte[] sign(@NonNull byte[] payload) throws IOException; + byte[] sign(@NonNull byte[] payload) throws FirebaseAuthException; /** * Returns the client email of the service account used to sign payloads. diff --git a/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java index 6ea70b880..c20c82a23 100644 --- a/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java +++ b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java @@ -9,9 +9,8 @@ import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpResponseInterceptor; import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.JsonObjectParser; -import com.google.api.client.util.Key; import com.google.api.client.util.StringUtils; import com.google.auth.ServiceAccountSigner; import com.google.auth.oauth2.GoogleCredentials; @@ -21,9 +20,13 @@ import com.google.common.io.BaseEncoding; import com.google.common.io.ByteStreams; import com.google.firebase.FirebaseApp; -import com.google.firebase.FirebaseOptions; +import com.google.firebase.FirebaseException; import com.google.firebase.ImplFirebaseTrampolines; -import com.google.firebase.internal.FirebaseRequestInitializer; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.internal.AbstractPlatformErrorHandler; +import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.ErrorHandlingHttpClient; +import com.google.firebase.internal.HttpRequestInfo; import com.google.firebase.internal.NonNull; import java.io.IOException; import java.util.Map; @@ -34,7 +37,9 @@ public class CryptoSigners { private static final String METADATA_SERVICE_URL = - "http://metadata/computeMetadata/v1/instance/service-accounts/default/email"; + "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email"; + + private CryptoSigners() { } /** * A {@link CryptoSigner} implementation that uses service account credentials or equivalent @@ -69,19 +74,22 @@ static class IAMCryptoSigner implements CryptoSigner { private static final String IAM_SIGN_BLOB_URL = "https://iam.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob"; - private final HttpRequestFactory requestFactory; - private final JsonFactory jsonFactory; private final String serviceAccount; + private final JsonFactory jsonFactory; + private final ErrorHandlingHttpClient httpClient; private HttpResponseInterceptor interceptor; IAMCryptoSigner( @NonNull HttpRequestFactory requestFactory, @NonNull JsonFactory jsonFactory, @NonNull String serviceAccount) { - this.requestFactory = checkNotNull(requestFactory); - this.jsonFactory = checkNotNull(jsonFactory); checkArgument(!Strings.isNullOrEmpty(serviceAccount)); this.serviceAccount = serviceAccount; + this.jsonFactory = checkNotNull(jsonFactory); + this.httpClient = new ErrorHandlingHttpClient<>( + requestFactory, + jsonFactory, + new IAMErrorHandler(jsonFactory)); } void setInterceptor(HttpResponseInterceptor interceptor) { @@ -89,28 +97,15 @@ void setInterceptor(HttpResponseInterceptor interceptor) { } @Override - public byte[] sign(byte[] payload) throws IOException { - String encodedUrl = String.format(IAM_SIGN_BLOB_URL, serviceAccount); - HttpResponse response = null; + public byte[] sign(byte[] payload) throws FirebaseAuthException { String encodedPayload = BaseEncoding.base64().encode(payload); Map content = ImmutableMap.of("bytesToSign", encodedPayload); - try { - HttpRequest request = requestFactory.buildPostRequest(new GenericUrl(encodedUrl), - new JsonHttpContent(jsonFactory, content)); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(interceptor); - response = request.execute(); - SignBlobResponse parsed = response.parseAs(SignBlobResponse.class); - return BaseEncoding.base64().decode(parsed.signature); - } finally { - if (response != null) { - try { - response.disconnect(); - } catch (IOException ignored) { - // Ignored - } - } - } + String encodedUrl = String.format(IAM_SIGN_BLOB_URL, serviceAccount); + HttpRequestInfo requestInfo = HttpRequestInfo + .buildPostRequest(encodedUrl, new JsonHttpContent(jsonFactory, content)) + .setResponseInterceptor(interceptor); + GenericJson parsed = httpClient.sendAndParse(requestInfo, GenericJson.class); + return BaseEncoding.base64().decode((String) parsed.get("signature")); } @Override @@ -119,9 +114,17 @@ public String getAccount() { } } - public static class SignBlobResponse { - @Key("signature") - private String signature; + private static class IAMErrorHandler + extends AbstractPlatformErrorHandler { + + IAMErrorHandler(JsonFactory jsonFactory) { + super(jsonFactory); + } + + @Override + protected FirebaseAuthException createException(FirebaseException base) { + return new FirebaseAuthException(base); + } } /** @@ -136,14 +139,12 @@ public static CryptoSigner getCryptoSigner(FirebaseApp firebaseApp) throws IOExc return new ServiceAccountCryptoSigner((ServiceAccountCredentials) credentials); } - FirebaseOptions options = firebaseApp.getOptions(); - HttpRequestFactory requestFactory = options.getHttpTransport().createRequestFactory( - new FirebaseRequestInitializer(firebaseApp)); - JsonFactory jsonFactory = options.getJsonFactory(); + HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(firebaseApp); + JsonFactory jsonFactory = firebaseApp.getOptions().getJsonFactory(); // If the SDK was initialized with a service account email, use it with the IAM service // to sign bytes. - String serviceAccountId = options.getServiceAccountId(); + String serviceAccountId = firebaseApp.getOptions().getServiceAccountId(); if (!Strings.isNullOrEmpty(serviceAccountId)) { return new IAMCryptoSigner(requestFactory, jsonFactory, serviceAccountId); } @@ -156,15 +157,22 @@ public static CryptoSigner getCryptoSigner(FirebaseApp firebaseApp) throws IOExc // Attempt to discover a service account email from the local Metadata service. Use it // with the IAM service to sign bytes. - HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(METADATA_SERVICE_URL)); + serviceAccountId = discoverServiceAccountId(firebaseApp); + return new IAMCryptoSigner(requestFactory, jsonFactory, serviceAccountId); + } + + private static String discoverServiceAccountId(FirebaseApp firebaseApp) throws IOException { + HttpRequestFactory metadataRequestFactory = + ApiClientUtils.newUnauthorizedRequestFactory(firebaseApp); + HttpRequest request = metadataRequestFactory.buildGetRequest( + new GenericUrl(METADATA_SERVICE_URL)); request.getHeaders().set("Metadata-Flavor", "Google"); HttpResponse response = request.execute(); try { byte[] output = ByteStreams.toByteArray(response.getContent()); - serviceAccountId = StringUtils.newStringUtf8(output).trim(); - return new IAMCryptoSigner(requestFactory, jsonFactory, serviceAccountId); + return StringUtils.newStringUtf8(output).trim(); } finally { - response.disconnect(); + ApiClientUtils.disconnectQuietly(response); } } } diff --git a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java b/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java index 95d313134..56be2015d 100644 --- a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java +++ b/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java @@ -25,8 +25,8 @@ import com.google.api.client.util.Base64; import com.google.api.client.util.Clock; import com.google.api.client.util.StringUtils; - import com.google.common.base.Strings; +import com.google.firebase.auth.FirebaseAuthException; import java.io.IOException; import java.util.Collection; import java.util.Map; @@ -48,12 +48,12 @@ public FirebaseTokenFactory(JsonFactory jsonFactory, Clock clock, CryptoSigner s this.signer = checkNotNull(signer); } - String createSignedCustomAuthTokenForUser(String uid) throws IOException { + String createSignedCustomAuthTokenForUser(String uid) throws FirebaseAuthException { return createSignedCustomAuthTokenForUser(uid, null); } public String createSignedCustomAuthTokenForUser( - String uid, Map developerClaims) throws IOException { + String uid, Map developerClaims) throws FirebaseAuthException { checkArgument(!Strings.isNullOrEmpty(uid), "Uid must be provided."); checkArgument(uid.length() <= 128, "Uid must be shorter than 128 characters."); @@ -77,20 +77,33 @@ public String createSignedCustomAuthTokenForUser( String.format("developerClaims must not contain a reserved key: %s", key)); } } + GenericJson jsonObject = new GenericJson(); jsonObject.putAll(developerClaims); payload.setDeveloperClaims(jsonObject); } + return signPayload(header, payload); } - private String signPayload(JsonWebSignature.Header header, - FirebaseCustomAuthToken.Payload payload) throws IOException { - String headerString = Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(header)); - String payloadString = Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(payload)); - String content = headerString + "." + payloadString; + private String signPayload( + JsonWebSignature.Header header, + FirebaseCustomAuthToken.Payload payload) throws FirebaseAuthException { + String content = encodePayload(header, payload); byte[] contentBytes = StringUtils.getBytesUtf8(content); String signature = Base64.encodeBase64URLSafeString(signer.sign(contentBytes)); return content + "." + signature; } + + private String encodePayload( + JsonWebSignature.Header header, FirebaseCustomAuthToken.Payload payload) { + try { + String headerString = Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(header)); + String payloadString = Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(payload)); + return headerString + "." + payloadString; + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to encode JWT with the given claims: " + e.getMessage(), e); + } + } } diff --git a/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java b/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java index bc98add3c..9c80d8b8b 100644 --- a/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java +++ b/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java @@ -18,10 +18,14 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpStatusCodes; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.auth.ServiceAccountSigner; @@ -29,21 +33,22 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.io.BaseEncoding; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.testing.MultiRequestMockHttpTransport; import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestResponseInterceptor; -import java.io.IOException; import org.junit.After; import org.junit.Test; public class CryptoSignersTest { @Test - public void testServiceAccountCryptoSigner() throws IOException { + public void testServiceAccountCryptoSigner() throws Exception { ServiceAccountCredentials credentials = ServiceAccountCredentials.fromStream( ServiceAccount.EDITOR.asStream()); byte[] expected = credentials.sign("foo".getBytes()); @@ -63,7 +68,7 @@ public void testInvalidServiceAccountCryptoSigner() { } @Test - public void testIAMCryptoSigner() throws IOException { + public void testIAMCryptoSigner() throws Exception { String signature = BaseEncoding.base64().encode("signed-bytes".getBytes()); String response = Utils.getDefaultJsonFactory().toString( ImmutableMap.of("signature", signature)); @@ -84,6 +89,29 @@ public void testIAMCryptoSigner() throws IOException { assertEquals(url, interceptor.getResponse().getRequest().getUrl().toString()); } + @Test + public void testIAMCryptoSignerHttpError() { + String error = "{\"error\": {\"status\":\"INTERNAL\", \"message\": \"Test error\"}}"; + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .setContent(error)) + .build(); + CryptoSigners.IAMCryptoSigner signer = new CryptoSigners.IAMCryptoSigner( + transport.createRequestFactory(), + Utils.getDefaultJsonFactory(), + "test-service-account@iam.gserviceaccount.com"); + try { + signer.sign("foo".getBytes()); + } catch (FirebaseAuthException e) { + assertEquals(ErrorCode.INTERNAL, e.getErrorCodeNew()); + assertEquals("Test error", e.getMessage()); + assertNotNull(e.getCause()); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); + } + } + @Test public void testInvalidIAMCryptoSigner() { try { @@ -119,7 +147,7 @@ public void testInvalidIAMCryptoSigner() { } @Test - public void testMetadataService() throws IOException { + public void testMetadataService() throws Exception { String signature = BaseEncoding.base64().encode("signed-bytes".getBytes()); String response = Utils.getDefaultJsonFactory().toString( ImmutableMap.of("signature", signature)); @@ -142,11 +170,13 @@ public void testMetadataService() throws IOException { assertArrayEquals("signed-bytes".getBytes(), data); final String url = "https://iam.googleapis.com/v1/projects/-/serviceAccounts/" + "metadata-server@iam.gserviceaccount.com:signBlob"; - assertEquals(url, interceptor.getResponse().getRequest().getUrl().toString()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals(url, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); } @Test - public void testExplicitServiceAccountEmail() throws IOException { + public void testExplicitServiceAccountEmail() throws Exception { String signature = BaseEncoding.base64().encode("signed-bytes".getBytes()); String response = Utils.getDefaultJsonFactory().toString( ImmutableMap.of("signature", signature)); @@ -170,11 +200,13 @@ public void testExplicitServiceAccountEmail() throws IOException { assertArrayEquals("signed-bytes".getBytes(), data); final String url = "https://iam.googleapis.com/v1/projects/-/serviceAccounts/" + "explicit-service-account@iam.gserviceaccount.com:signBlob"; - assertEquals(url, interceptor.getResponse().getRequest().getUrl().toString()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals(url, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); } @Test - public void testCredentialsWithSigner() throws IOException { + public void testCredentialsWithSigner() throws Exception { // Should fall back to signing-enabled credential FirebaseOptions options = new FirebaseOptions.Builder() .setCredentials(new MockGoogleCredentialsWithSigner("test-token")) diff --git a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java index 71b95bee8..7c75f64a1 100644 --- a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java +++ b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java @@ -28,8 +28,9 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; +import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.testing.TestUtils; -import java.io.IOException; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -138,12 +139,12 @@ private static class TestCryptoSigner implements CryptoSigner { } @Override - public byte[] sign(byte[] payload) throws IOException { + public byte[] sign(byte[] payload) throws FirebaseAuthException { try { return SecurityUtils.sign(SecurityUtils.getSha256WithRsaSignatureAlgorithm(), privateKey, payload); } catch (GeneralSecurityException e) { - throw new IOException(e); + throw new FirebaseAuthException(ErrorCode.UNKNOWN, "Failed to sign token", e, null, null); } }