Skip to content

Error handling revamp for the custom token creation API #366

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions src/main/java/com/google/firebase/auth/AuthErrorHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
10 changes: 1 addition & 9 deletions src/main/java/com/google/firebase/auth/FirebaseAuth.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);

Expand Down Expand Up @@ -331,12 +328,7 @@ private CallableOperation<String, FirebaseAuthException> createCustomTokenOp(
return new CallableOperation<String, FirebaseAuthException>() {
@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);
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,7 +30,7 @@ public class FirebaseAuthException extends FirebaseException {

private final AuthErrorCode errorCode;

FirebaseAuthException(
public FirebaseAuthException(
@NonNull ErrorCode errorCode,
@NonNull String message,
Throwable cause,
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
Expand Down
90 changes: 49 additions & 41 deletions src/main/java/com/google/firebase/auth/internal/CryptoSigners.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -69,48 +74,38 @@ 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<FirebaseAuthException> 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) {
this.interceptor = 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<String, String> 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
Expand All @@ -119,9 +114,17 @@ public String getAccount() {
}
}

public static class SignBlobResponse {
@Key("signature")
private String signature;
private static class IAMErrorHandler
extends AbstractPlatformErrorHandler<FirebaseAuthException> {

IAMErrorHandler(JsonFactory jsonFactory) {
super(jsonFactory);
}

@Override
protected FirebaseAuthException createException(FirebaseException base) {
return new FirebaseAuthException(base);
}
}

/**
Expand All @@ -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);
}
Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String, Object> developerClaims) throws IOException {
String uid, Map<String, Object> developerClaims) throws FirebaseAuthException {
checkArgument(!Strings.isNullOrEmpty(uid), "Uid must be provided.");
checkArgument(uid.length() <= 128, "Uid must be shorter than 128 characters.");

Expand All @@ -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);
}
}
}
Loading