Skip to content
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Unreleased

- [added] Implemented the ability to create custom tokens without
service account credentials.
- [added] Added the `setServiceAccount()` method to the
`FirebaseOptions.Builder` API.
- [added] The SDK can now read the Firebase/GCP project ID from both
`GCLOUD_PROJECT` and `GOOGLE_CLOUD_PROJECT` environment variables.

Expand Down
37 changes: 37 additions & 0 deletions src/main/java/com/google/firebase/FirebaseOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public final class FirebaseOptions {
private final GoogleCredentials credentials;
private final Map<String, Object> databaseAuthVariableOverride;
private final String projectId;
private final String serviceAccountId;
private final HttpTransport httpTransport;
private final int connectTimeout;
private final int readTimeout;
Expand All @@ -77,6 +78,11 @@ private FirebaseOptions(@NonNull FirebaseOptions.Builder builder) {
checkArgument(!builder.storageBucket.startsWith("gs://"),
"StorageBucket must not include 'gs://' prefix.");
}
if (!Strings.isNullOrEmpty(builder.serviceAccountId)) {
this.serviceAccountId = builder.serviceAccountId;
} else {
this.serviceAccountId = null;
}
this.storageBucket = builder.storageBucket;
this.httpTransport = checkNotNull(builder.httpTransport,
"FirebaseOptions must be initialized with a non-null HttpTransport.");
Expand Down Expand Up @@ -131,6 +137,16 @@ public String getProjectId() {
return projectId;
}

/**
* Returns the client email address of the service account.
*
* @return The client email of the service account set via
* {@link Builder#setServiceAccountId(String)}
*/
public String getServiceAccountId() {
return serviceAccountId;
}

/**
* Returns the <code>HttpTransport</code> used to call remote HTTP endpoints. This transport is
* used by all services of the SDK, except for FirebaseDatabase.
Expand Down Expand Up @@ -192,6 +208,9 @@ public static final class Builder {

@Key("storageBucket")
private String storageBucket;

@Key("serviceAccountId")
private String serviceAccountId;

private GoogleCredentials credentials;
private HttpTransport httpTransport = Utils.getDefaultTransport();
Expand Down Expand Up @@ -310,6 +329,24 @@ public Builder setProjectId(@NonNull String projectId) {
return this;
}

/**
* Sets the client email address of the service account that should be associated with an app.
*
* <p>This is used to <a href="https://firebase.google.com/docs/auth/admin/create-custom-tokens">
* create custom auth tokens</a> when service account credentials are not available. The client
* email address of a service account can be found in the {@code client_email} field of the
* service account JSON.
*
* @param serviceAccountId A service account email address string.
* @return This <code>Builder</code> instance is returned so subsequent calls can be chained.
*/
public Builder setServiceAccountId(@NonNull String serviceAccountId) {
checkArgument(!Strings.isNullOrEmpty(serviceAccountId),
"Service account ID must not be null or empty");
this.serviceAccountId = serviceAccountId;
return this;
}

/**
* Sets the <code>HttpTransport</code> used to make remote HTTP calls. A reasonable default
* is used if not explicitly set. The transport specified by calling this method is
Expand Down
67 changes: 47 additions & 20 deletions src/main/java/com/google/firebase/auth/FirebaseAuth.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@
import com.google.api.client.json.JsonFactory;
import com.google.api.client.util.Clock;
import com.google.api.core.ApiFuture;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.firebase.FirebaseApp;
Expand All @@ -42,10 +40,10 @@
import com.google.firebase.internal.NonNull;
import com.google.firebase.internal.Nullable;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

/**
* This class is the entry point for all server-side Firebase Authentication actions.
Expand All @@ -65,10 +63,10 @@ public class FirebaseAuth {

private final FirebaseApp firebaseApp;
private final KeyManagers keyManagers;
private final GoogleCredentials credentials;
private final String projectId;
private final JsonFactory jsonFactory;
private final FirebaseUserManager userManager;
private final AtomicReference<FirebaseTokenFactory> tokenFactory;
private final AtomicBoolean destroyed;
private final Object lock;

Expand All @@ -85,10 +83,10 @@ private FirebaseAuth(FirebaseApp firebaseApp) {
this.firebaseApp = checkNotNull(firebaseApp);
this.keyManagers = checkNotNull(keyManagers);
this.clock = checkNotNull(clock);
this.credentials = ImplFirebaseTrampolines.getCredentials(firebaseApp);
this.projectId = ImplFirebaseTrampolines.getProjectId(firebaseApp);
this.jsonFactory = firebaseApp.getOptions().getJsonFactory();
this.userManager = new FirebaseUserManager(firebaseApp);
this.tokenFactory = new AtomicReference<>(null);
this.destroyed = new AtomicBoolean(false);
this.lock = new Object();
}
Expand Down Expand Up @@ -287,17 +285,31 @@ public String createCustomToken(@NonNull String uid) throws FirebaseAuthExceptio
* <a href="/docs/auth/admin/create-custom-tokens#sign_in_using_custom_tokens_on_clients">signInWithCustomToken</a>
* authentication API.
*
* <p>{@link FirebaseApp} must have been initialized with service account credentials to use
* call this method.
* <p>This method attempts to generate a token using:
* <ol>
* <li>the private key of {@link FirebaseApp}'s service account credentials, if provided at
* initialization.
* <li>the <a href="https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signBlob">IAM service</a>
* if a service account email was specified via
* {@link com.google.firebase.FirebaseOptions.Builder#setServiceAccountId(String)}.
* <li>the <a href="https://cloud.google.com/appengine/docs/standard/java/appidentity/">App Identity
* service</a> if the code is deployed in the Google App Engine standard environment.
* <li>the <a href="https://cloud.google.com/compute/docs/storing-retrieving-metadata">
* local Metadata server</a> if the code is deployed in a different GCP-managed environment
* like Google Compute Engine.
* </ol>
*
* <p>This method throws an exception when all the above fail.
*
* @param uid The UID to store in the token. This identifies the user to other Firebase services
* (Realtime Database, Firebase Auth, etc.). Should be less than 128 characters.
* @param developerClaims Additional claims to be stored in the token (and made available to
* security rules in Database, Storage, etc.). These must be able to be serialized to JSON
* (e.g. contain only Maps, Arrays, Strings, Booleans, Numbers, etc.)
* @return A Firebase custom token string.
* @throws IllegalArgumentException If the specified uid is null or empty, or if the app has not
* been initialized with service account credentials.
* @throws IllegalArgumentException If the specified uid is null or empty.
* @throws IllegalStateException If the SDK fails to discover a viable approach for signing
* tokens.
* @throws FirebaseAuthException If an error occurs while generating the custom token.
*/
public String createCustomToken(@NonNull String uid,
Expand Down Expand Up @@ -342,28 +354,43 @@ private CallableOperation<String, FirebaseAuthException> createCustomTokenOp(
final String uid, final Map<String, Object> developerClaims) {
checkNotDestroyed();
checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
checkArgument(credentials instanceof ServiceAccountCredentials,
"Must initialize FirebaseApp with a service account credential to call "
+ "createCustomToken()");
final FirebaseTokenFactory tokenFactory = ensureTokenFactory();
return new CallableOperation<String, FirebaseAuthException>() {
@Override
public String execute() throws FirebaseAuthException {
final ServiceAccountCredentials serviceAccount = (ServiceAccountCredentials) credentials;
FirebaseTokenFactory tokenFactory = FirebaseTokenFactory.getInstance();
try {
return tokenFactory.createSignedCustomAuthTokenForUser(
uid,
developerClaims,
serviceAccount.getClientEmail(),
serviceAccount.getPrivateKey());
} catch (GeneralSecurityException | IOException e) {
return tokenFactory.createSignedCustomAuthTokenForUser(uid, developerClaims);
} catch (IOException e) {
throw new FirebaseAuthException(ERROR_CUSTOM_TOKEN,
"Failed to generate a custom token", e);
}
}
};
}

private FirebaseTokenFactory ensureTokenFactory() {
FirebaseTokenFactory result = this.tokenFactory.get();
if (result == null) {
synchronized (lock) {
result = this.tokenFactory.get();

@schmidt-sebastian schmidt-sebastian Jun 13, 2018

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remember my Java 101 class correctly, then you don't need to use an AtomicReference here since you are grabbing a lock already.

http://www.cs.umd.edu/users/pugh/java/memoryModel/jsr-133-faq.html#synchronization
"Synchronization ensures that memory writes by a thread before or during a synchronized block are made visible in a predictable manner to other threads which synchronize on the same monitor. "

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then we'd have to make the variable volatile, to get the double-checked locking semantics to work correctly: https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

I don't really have a preference. Drop the AtomicRef and switch to a volatile variable?

if (result == null) {
try {
result = FirebaseTokenFactory.fromApp(firebaseApp, clock);
this.tokenFactory.set(result);
} catch (IOException e) {
throw new IllegalStateException(
"Failed to initialize FirebaseTokenFactory. Make sure to initialize the SDK "
+ "with service account credentials or specify a service account "
+ "ID with iam.serviceAccounts.signBlob permission. Please refer to "
+ "https://firebase.google.com/docs/auth/admin/create-custom-tokens for more "
+ "details on creating custom tokens.", e);
}
}
}
}
return result;
}

/**
* Parses and verifies a Firebase ID Token.
*
Expand Down
47 changes: 47 additions & 0 deletions src/main/java/com/google/firebase/auth/internal/CryptoSigner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2018 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.auth.internal;

import com.google.firebase.internal.NonNull;
import java.io.IOException;

/**
* Represents an object that can be used to cryptographically sign data. Mainly used for signing
* custom JWT tokens issued to Firebase users.
*
* <p>See {@link com.google.firebase.auth.FirebaseAuth#createCustomToken(String)}.
*/
interface CryptoSigner {

/**
* Signs the given payload.
*
* @param payload Data to be signed
* @return Signature as a byte array
* @throws IOException If an error occurs during signing
*/
@NonNull
byte[] sign(@NonNull byte[] payload) throws IOException;

/**
* Returns the client email of the service account used to sign payloads.
*
* @return A service account client email
*/
@NonNull
String getAccount();
}
Loading