Repository: firebase/firebase-admin-java
Pull request #60 (2017-08-02 18:20:31)

Implementing Phone Auth Support
* Added `FirebaseAuth.getUserByPhoneNumber()` method.
* Added `setPhoneNumber()` method to `CreateRequest` and `UpdateRequest` types.
* Exposing phone number via `UserInfo` and `UserRecord` types.

go/firebase-java-phone-auth

hiranya911: Related to #56 
hiranya911: Made the suggested changes.

Patch:
diff --git a/pom.xml b/pom.xml
index 20133de..c718b61 100644
--- a/pom.xml
+++ b/pom.xml
@@ -329,6 +329,11 @@
             <artifactId>guava</artifactId>
             <version>20.0</version>
         </dependency>
+        <dependency>
+            <groupId>com.google.cloud</groupId>
+            <artifactId>google-cloud-storage</artifactId>
+            <version>1.2.1</version>
+        </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
diff --git a/src/main/java/com/google/firebase/FirebaseOptions.java b/src/main/java/com/google/firebase/FirebaseOptions.java
index f37edfa..1c10939 100644
--- a/src/main/java/com/google/firebase/FirebaseOptions.java
+++ b/src/main/java/com/google/firebase/FirebaseOptions.java
@@ -16,12 +16,13 @@
 
 package com.google.firebase;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.api.client.googleapis.util.Utils;
 import com.google.api.client.http.HttpTransport;
 import com.google.api.client.json.JsonFactory;
-import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
 import com.google.firebase.auth.FirebaseCredential;
 import com.google.firebase.auth.FirebaseCredentials;
 import com.google.firebase.internal.NonNull;
@@ -36,6 +37,7 @@ public final class FirebaseOptions {
   // TODO: deprecate and remove it once we can fetch these from Remote Config.
 
   private final String databaseUrl;
+  private final String storageBucket;
   private final FirebaseCredential firebaseCredential;
   private final Map<String, Object> databaseAuthVariableOverride;
   private final HttpTransport httpTransport;
@@ -46,6 +48,7 @@ public final class FirebaseOptions {
         "FirebaseOptions must be initialized with setCredential().");
     this.databaseUrl = builder.databaseUrl;
     this.databaseAuthVariableOverride = builder.databaseAuthVariableOverride;
+    this.storageBucket = builder.storageBucket;
     this.httpTransport = checkNotNull(builder.httpTransport,
         "FirebaseOptions must be initialized with a non-null HttpTransport.");
     this.jsonFactory = checkNotNull(builder.jsonFactory,
@@ -61,6 +64,15 @@ public final class FirebaseOptions {
     return databaseUrl;
   }
 
+  /**
+   * Returns the name of the Google Cloud Storage bucket used for storing application data.
+   *
+   * @return The cloud storage bucket name set via {@link Builder#setStorageBucket}
+   */
+  public String getStorageBucket() {
+    return storageBucket;
+  }
+
   FirebaseCredential getCredential() {
     return firebaseCredential;
   }
@@ -102,6 +114,7 @@ public final class FirebaseOptions {
   public static final class Builder {
 
     private String databaseUrl;
+    private String storageBucket;
     private FirebaseCredential firebaseCredential;
     private Map<String, Object> databaseAuthVariableOverride = new HashMap<>();
     private HttpTransport httpTransport = Utils.getDefaultTransport();
@@ -118,6 +131,7 @@ public final class FirebaseOptions {
      */
     public Builder(FirebaseOptions options) {
       databaseUrl = options.databaseUrl;
+      storageBucket = options.storageBucket;
       firebaseCredential = options.firebaseCredential;
       databaseAuthVariableOverride = options.databaseAuthVariableOverride;
       httpTransport = options.httpTransport;
@@ -127,8 +141,8 @@ public final class FirebaseOptions {
     /**
      * Sets the Realtime Database URL to use for data storage.
      *
-     * <p>See <a href="/docs/admin/setup#initialize_the_sdk">Initialize the SDK</a> for code samples
-     * and detailed documentation.
+     * <p>See <a href="https://firebase.google.com/docs/admin/setup#initialize_the_sdk">
+     * Initialize the SDK</a> for code samples and detailed documentation.
      *
      * @param databaseUrl The Realtime Database URL to use for data storage.
      * @return This <code>Builder</code> instance is returned so subsequent calls can be chained.
@@ -138,11 +152,29 @@ public final class FirebaseOptions {
       return this;
     }
 
+    /**
+     * Sets the name of the Google Cloud Storage bucket for reading and writing application data.
+     * The same credential used to initialize the SDK (see {@link Builder#setCredential}) will be
+     * used to access the bucket.
+     *
+     * <p>See <a href="https://firebase.google.com/docs/admin/setup#initialize_the_sdk">
+     * Initialize the SDK</a> for code samples and detailed documentation.
+     *
+     * @param storageBucket The name of an existing Google Cloud Storage bucket.
+     * @return This <code>Builder</code> instance is returned so subsequent calls can be chained.
+     */
+    public Builder setStorageBucket(String storageBucket) {
+      checkArgument(!Strings.isNullOrEmpty(storageBucket),
+          "Storage bucket must not be null or empty");
+      this.storageBucket = storageBucket;
+      return this;
+    }
+
     /**
      * Sets the <code>FirebaseCredential</code> to use to authenticate the SDK.
      *
-     * <p>See <a href="/docs/admin/setup#initialize_the_sdk">Initialize the SDK</a> for code samples
-     * and detailed documentation.
+     * <p>See <a href="https://firebase.google.com/docs/admin/setup#initialize_the_sdk">
+     * Initialize the SDK</a> for code samples and detailed documentation.
      *
      * @param credential A <code>FirebaseCredential</code> used to authenticate the SDK. See {@link
      *     FirebaseCredentials} for default implementations.
@@ -164,8 +196,8 @@ public final class FirebaseOptions {
      * instance. If this option is set to <code>null</code>, security rules are evaluated against an
      * unauthenticated user. That is, the <code>auth</code> variable is <code>null</code>.
      *
-     * <p>See <a href="/docs/database/admin/start#authenticate-with-limited-privileges">Authenticate
-     * with limited privileges</a> for code samples and detailed documentation.
+     * <p>See <a href="https://firebase.google.com/docs/database/admin/start#authenticate-with-limited-privileges">
+     * Authenticate with limited privileges</a> for code samples and detailed documentation.
      *
      * @param databaseAuthVariableOverride The value to use for the <code>auth</code> variable in
      *     the security rules for Realtime Database actions.
diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
index 804bb36..6796231 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
@@ -243,6 +243,26 @@ public class FirebaseAuth {
         });
   }
 
+  /**
+   * Gets the user data corresponding to the specified user phone number.
+   *
+   * @param phoneNumber A user phone number string.
+   * @return A {@link Task} which will complete successfully with a {@link UserRecord} instance.
+   *     If an error occurs while retrieving user data or if the phone number does not
+   *     correspond to a user, the task fails with a FirebaseAuthException.
+   * @throws IllegalArgumentException If the phone number is null or empty.
+   */
+  public Task<UserRecord> getUserByPhoneNumber(final String phoneNumber) {
+    checkArgument(!Strings.isNullOrEmpty(phoneNumber), "phone number must not be null or empty");
+    return ImplFirebaseTrampolines.getToken(firebaseApp, false).continueWith(
+        new Continuation<GetTokenResult, UserRecord>() {
+          @Override
+          public UserRecord then(Task<GetTokenResult> task) throws Exception {
+            return userManager.getUserByPhoneNumber(phoneNumber, task.getResult().getToken());
+          }
+        });
+  }
+
   /**
    * Creates a new user account with the attributes contained in the specified
    * {@link CreateRequest}.
diff --git a/src/main/java/com/google/firebase/auth/FirebaseCredentials.java b/src/main/java/com/google/firebase/auth/FirebaseCredentials.java
index f5217ea..072fd58 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseCredentials.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseCredentials.java
@@ -48,9 +48,17 @@ public class FirebaseCredentials {
 
   private static final List<String> FIREBASE_SCOPES =
       ImmutableList.of(
+          // Enables access to Firebase Realtime Database.
           "https://www.googleapis.com/auth/firebase.database",
+
+          // Enables access to the email address associated with a project.
           "https://www.googleapis.com/auth/userinfo.email",
-          "https://www.googleapis.com/auth/identitytoolkit");
+
+          // Enables access to Google Identity Toolkit (for user management APIs).
+          "https://www.googleapis.com/auth/identitytoolkit",
+
+          // Enables access to Google Cloud Storage.
+          "https://www.googleapis.com/auth/devstorage.full_control");
 
   private FirebaseCredentials() {
   }
diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
index 658124e..df56a8c 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
@@ -119,6 +119,24 @@ class FirebaseUserManager {
     return new UserRecord(response.getUsers().get(0));
   }
 
+  UserRecord getUserByPhoneNumber(String phoneNumber, String token) throws FirebaseAuthException {
+    final Map<String, Object> payload = ImmutableMap.<String, Object>of(
+        "phoneNumber", ImmutableList.of(phoneNumber));
+    GetAccountInfoResponse response;
+    try {
+      response = post("getAccountInfo", token, payload, GetAccountInfoResponse.class);
+    } catch (IOException e) {
+      throw new FirebaseAuthException(INTERNAL_ERROR,
+          "IO error while retrieving user with phone number: " + phoneNumber, e);
+    }
+
+    if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) {
+      throw new FirebaseAuthException(USER_NOT_FOUND_ERROR,
+          "No user record found for the provided phone number: " + phoneNumber);
+    }
+    return new UserRecord(response.getUsers().get(0));
+  }
+
   String createUser(CreateRequest request, String token) throws FirebaseAuthException {
     GenericJson response;
     try {
diff --git a/src/main/java/com/google/firebase/auth/ProviderUserInfo.java b/src/main/java/com/google/firebase/auth/ProviderUserInfo.java
index 07ed154..d9663e2 100644
--- a/src/main/java/com/google/firebase/auth/ProviderUserInfo.java
+++ b/src/main/java/com/google/firebase/auth/ProviderUserInfo.java
@@ -28,6 +28,7 @@ class ProviderUserInfo implements UserInfo {
   private final String uid;
   private final String displayName;
   private final String email;
+  private final String phoneNumber;
   private final String photoUrl;
   private final String providerId;
 
@@ -35,6 +36,7 @@ class ProviderUserInfo implements UserInfo {
     this.uid = response.getUid();
     this.displayName = response.getDisplayName();
     this.email = response.getEmail();
+    this.phoneNumber = response.getPhoneNumber();
     this.photoUrl = response.getPhotoUrl();
     this.providerId = response.getProviderId();
   }
@@ -56,6 +58,12 @@ class ProviderUserInfo implements UserInfo {
     return email;
   }
 
+  @Nullable
+  @Override
+  public String getPhoneNumber() {
+    return phoneNumber;
+  }
+
   @Nullable
   @Override
   public String getPhotoUrl() {
diff --git a/src/main/java/com/google/firebase/auth/UserInfo.java b/src/main/java/com/google/firebase/auth/UserInfo.java
index e433400..a95e5ec 100644
--- a/src/main/java/com/google/firebase/auth/UserInfo.java
+++ b/src/main/java/com/google/firebase/auth/UserInfo.java
@@ -47,6 +47,14 @@ public interface UserInfo {
   @Nullable
   String getEmail();
 
+  /**
+   * Returns the user's phone number, if available.
+   *
+   * @return a phone number string or null.
+   */
+  @Nullable
+  String getPhoneNumber();
+
   /**
    * Returns the user's photo URL, if available.
    *
diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java
index e672054..6d650e1 100644
--- a/src/main/java/com/google/firebase/auth/UserRecord.java
+++ b/src/main/java/com/google/firebase/auth/UserRecord.java
@@ -18,6 +18,7 @@ package com.google.firebase.auth;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
@@ -45,6 +46,7 @@ public class UserRecord implements UserInfo {
 
   private final String uid;
   private final String email;
+  private final String phoneNumber;
   private final boolean emailVerified;
   private final String displayName;
   private final String photoUrl;
@@ -57,6 +59,7 @@ public class UserRecord implements UserInfo {
     checkArgument(!Strings.isNullOrEmpty(response.getUid()), "uid must not be null or empty");
     this.uid = response.getUid();
     this.email = response.getEmail();
+    this.phoneNumber = response.getPhoneNumber();
     this.emailVerified = response.isEmailVerified();
     this.displayName = response.getDisplayName();
     this.photoUrl = response.getPhotoUrl();
@@ -103,6 +106,17 @@ public class UserRecord implements UserInfo {
     return email;
   }
 
+  /**
+   * Returns the phone number associated with this user.
+   *
+   * @return a phone number string or null.
+   */
+  @Nullable
+  @Override
+  public String getPhoneNumber() {
+    return phoneNumber;
+  }
+
   /**
    * Returns whether the email address of this user has been verified.
    *
@@ -177,6 +191,14 @@ public class UserRecord implements UserInfo {
     checkArgument(email.matches("^[^@]+@[^@]+$"));
   }
 
+  private static void checkPhoneNumber(String phoneNumber) {
+    // Phone number verification is very lax here. Backend will enforce E.164 spec compliance, and
+    // normalize accordingly.
+    checkArgument(!Strings.isNullOrEmpty(phoneNumber), "phone number cannot be null or empty");
+    checkState(phoneNumber.startsWith("+"),
+        "phone number must be a valid, E.164 compliant identifier starting with a '+' sign");
+  }
+
   private static void checkPassword(String password) {
     checkArgument(!Strings.isNullOrEmpty(password), "password cannot be null or empty");
     checkArgument(password.length() >= 6, "password must be at least 6 characters long");
@@ -223,6 +245,17 @@ public class UserRecord implements UserInfo {
       return this;
     }
 
+    /**
+     * Sets a phone number for the new user.
+     *
+     * @param phone a non-null, non-empty phone number string.
+     */
+    public CreateRequest setPhoneNumber(String phone) {
+      checkPhoneNumber(phone);
+      properties.put("phoneNumber", phone);
+      return this;
+    }
+
     /**
      * Sets whether the user email address has been verified or not.
      *
@@ -323,6 +356,20 @@ public class UserRecord implements UserInfo {
       return this;
     }
 
+    /**
+     * Updates the phone number associated with this user. Calling this method with a null argument
+     * removes the phone number from the user account.
+     *
+     * @param phone a valid phone number string or null.
+     */
+    public UpdateRequest setPhoneNumber(@Nullable String phone) {
+      if (phone != null) {
+        checkPhoneNumber(phone);
+      }
+      properties.put("phoneNumber", phone);
+      return this;
+    }
+
     /**
      * Updates the email verification status of this account.
      *
@@ -339,7 +386,7 @@ public class UserRecord implements UserInfo {
      *
      * @param displayName a display name string or null
      */
-    public UpdateRequest setDisplayName(String displayName) {
+    public UpdateRequest setDisplayName(@Nullable String displayName) {
       properties.put("displayName", displayName);
       return this;
     }
@@ -350,7 +397,7 @@ public class UserRecord implements UserInfo {
      *
      * @param photoUrl a valid URL string or null
      */
-    public UpdateRequest setPhotoUrl(String photoUrl) {
+    public UpdateRequest setPhotoUrl(@Nullable String photoUrl) {
       if (photoUrl != null) {
         try {
           new URL(photoUrl);
@@ -396,6 +443,11 @@ public class UserRecord implements UserInfo {
       if (!remove.isEmpty()) {
         copy.put("deleteAttribute", ImmutableList.copyOf(remove));
       }
+
+      if (copy.containsKey("phoneNumber") && copy.get("phoneNumber") == null) {
+        copy.put("deleteProvider", ImmutableList.of("phone"));
+        copy.remove("phoneNumber");
+      }
       return ImmutableMap.copyOf(copy);
     }
   }
diff --git a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java
index f169897..f648667 100644
--- a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java
+++ b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java
@@ -49,6 +49,9 @@ public final class GetAccountInfoResponse {
     @Key("email")
     private String email;
 
+    @Key("phoneNumber")
+    private String phoneNumber;
+
     @Key("emailVerified")
     private boolean emailVerified;
 
@@ -78,6 +81,10 @@ public final class GetAccountInfoResponse {
       return email;
     }
 
+    public String getPhoneNumber() {
+      return phoneNumber;
+    }
+
     public boolean isEmailVerified() {
       return emailVerified;
     }
@@ -121,6 +128,9 @@ public final class GetAccountInfoResponse {
     @Key("email")
     private String email;
 
+    @Key("phoneNumber")
+    private String phoneNumber;
+
     @Key("photoUrl")
     private String photoUrl;
 
@@ -139,6 +149,10 @@ public final class GetAccountInfoResponse {
       return email;
     }
 
+    public String getPhoneNumber() {
+      return phoneNumber;
+    }
+
     public String getPhotoUrl() {
       return photoUrl;
     }
diff --git a/src/main/java/com/google/firebase/cloud/StorageClient.java b/src/main/java/com/google/firebase/cloud/StorageClient.java
new file mode 100644
index 0000000..0c3c67d
--- /dev/null
+++ b/src/main/java/com/google/firebase/cloud/StorageClient.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2017 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.cloud;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.cloud.storage.Bucket;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageOptions;
+import com.google.common.base.Strings;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.ImplFirebaseTrampolines;
+import com.google.firebase.internal.FirebaseCloudCredentials;
+import com.google.firebase.internal.FirebaseService;
+
+/**
+ * StorageClient provides access to Google Cloud Storage APIs. You can specify a default cloud
+ * storage bucket via {@link com.google.firebase.FirebaseOptions}, and then get a reference to this
+ * default bucket by calling {@link StorageClient#bucket()}. Or you can get a reference to a
+ * specific bucket at any time by calling {@link StorageClient#bucket(String)}.
+ *
+ * <p>This class requires Google Cloud Storage libraries for Java. Make sure the artifact
+ * google-cloud-storage is in the classpath along with its transitive dependencies.
+ */
+public class StorageClient {
+
+  private final FirebaseApp app;
+  private final Storage storage;
+
+  private StorageClient(FirebaseApp app) {
+    this.app = checkNotNull(app, "FirebaseApp must not be null");
+    this.storage = StorageOptions.newBuilder()
+        .setCredentials(new FirebaseCloudCredentials(app))
+        .build()
+        .getService();
+  }
+
+  public static StorageClient getInstance() {
+    return getInstance(FirebaseApp.getInstance());
+  }
+
+  public static synchronized StorageClient getInstance(FirebaseApp app) {
+    StorageClientService service = ImplFirebaseTrampolines.getService(app, SERVICE_ID,
+        StorageClientService.class);
+    if (service == null) {
+      service = ImplFirebaseTrampolines.addService(app, new StorageClientService(app));
+    }
+    return service.getInstance();
+  }
+
+  /**
+   * Returns the default cloud storage bucket associated with the current app. This is the bucket
+   * configured via {@link com.google.firebase.FirebaseOptions} when initializing the app. If
+   * no bucket was configured via options, this method throws an exception.
+   *
+   * @return a cloud storage Bucket instance, or null if the configured bucket does not exist.
+   * @throws IllegalArgumentException If no bucket is configured via <code>FirebaseOptions</code>,
+   *     or if the bucket does not exist.
+   */
+  public Bucket bucket() {
+    return bucket(app.getOptions().getStorageBucket());
+  }
+
+  /**
+   * Returns a cloud storage Bucket instance for the specified bucket name.
+   *
+   * @param name a non-null, non-empty bucket name.
+   * @return a cloud storage Bucket instance, or null if the specified bucket does not exist.
+   * @throws IllegalArgumentException If the bucket name is null, empty, or if the specified
+   *     bucket does not exist.
+   */
+  public Bucket bucket(String name) {
+    checkArgument(!Strings.isNullOrEmpty(name),
+        "Bucket name not specified. Specify the bucket name via the storageBucket "
+            + "option when initializing the app, or specify the bucket name explicitly when "
+            + "calling the getBucket() method.");
+    Bucket bucket = storage.get(name);
+    checkArgument(bucket != null, "Bucket " + name + " does not exist.");
+    return bucket;
+  }
+
+  private static final String SERVICE_ID = StorageClient.class.getName();
+
+  private static class StorageClientService extends FirebaseService<StorageClient> {
+
+    StorageClientService(FirebaseApp app) {
+      super(SERVICE_ID, new StorageClient(app));
+    }
+
+    @Override
+    public void destroy() {
+      // NOTE: We don't explicitly tear down anything here, but public methods of StorageClient
+      // will now fail because calls to getOptions() and getToken() will hit FirebaseApp,
+      // which will throw once the app is deleted.
+    }
+  }
+
+}
diff --git a/src/main/java/com/google/firebase/internal/FirebaseCloudCredentials.java b/src/main/java/com/google/firebase/internal/FirebaseCloudCredentials.java
new file mode 100644
index 0000000..5786342
--- /dev/null
+++ b/src/main/java/com/google/firebase/internal/FirebaseCloudCredentials.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2017 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.internal;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.auth.Credentials;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.ImplFirebaseTrampolines;
+import com.google.firebase.tasks.Continuation;
+import com.google.firebase.tasks.Task;
+import com.google.firebase.tasks.Tasks;
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * A Google cloud credential implementation that uses OAuth2 access tokens obtained from a
+ * <code>FirebaseApp</code> to authenticate cloud API calls. This essentially acts as a bridge
+ * between Firebase Admin SDK APIs and the Google cloud <code>Credentials</code> API.
+ */
+public final class FirebaseCloudCredentials extends Credentials {
+
+  private final FirebaseApp app;
+
+  public FirebaseCloudCredentials(FirebaseApp app) {
+    this.app = checkNotNull(app);
+  }
+
+  @Override
+  public String getAuthenticationType() {
+    return "OAuth2";
+  }
+
+  @Override
+  public Map<String, List<String>> getRequestMetadata(URI uri) throws IOException {
+    Task<String> task = ImplFirebaseTrampolines.getToken(app, false).continueWith(
+        new Continuation<GetTokenResult, String>() {
+          @Override
+          public String then(Task<GetTokenResult> task) throws Exception {
+            return task.getResult().getToken();
+          }
+        });
+    try {
+      String authHeader = "Bearer " + Tasks.await(task);
+      return ImmutableMap.<String, List<String>>of(
+          "Authorization", ImmutableList.of(authHeader));
+    } catch (ExecutionException | InterruptedException e) {
+      throw new IOException("Failed to acquire an OAuth token", e);
+    }
+  }
+
+  @Override
+  public boolean hasRequestMetadata() {
+    return true;
+  }
+
+  @Override
+  public boolean hasRequestMetadataOnly() {
+    return true;
+  }
+
+  @Override
+  public void refresh() throws IOException {
+    ImplFirebaseTrampolines.getToken(app, true);
+  }
+
+}


Test patch:
diff --git a/src/test/java/com/google/firebase/FirebaseOptionsTest.java b/src/test/java/com/google/firebase/FirebaseOptionsTest.java
index b59561f..5791947 100644
--- a/src/test/java/com/google/firebase/FirebaseOptionsTest.java
+++ b/src/test/java/com/google/firebase/FirebaseOptionsTest.java
@@ -20,6 +20,7 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.fail;
 
@@ -45,6 +46,7 @@ import org.junit.Test;
 public class FirebaseOptionsTest {
 
   private static final String FIREBASE_DB_URL = "https://mock-project.firebaseio.com";
+  private static final String FIREBASE_STORAGE_BUCKET = "mock-storage-bucket";
 
   private static final FirebaseOptions ALL_VALUES_OPTIONS =
       new FirebaseOptions.Builder()
@@ -60,11 +62,13 @@ public class FirebaseOptionsTest {
     FirebaseOptions firebaseOptions =
         new FirebaseOptions.Builder()
             .setDatabaseUrl(FIREBASE_DB_URL)
+            .setStorageBucket(FIREBASE_STORAGE_BUCKET)
             .setCredential(FirebaseCredentials.fromCertificate(ServiceAccount.EDITOR.asStream()))
             .setJsonFactory(jsonFactory)
             .setHttpTransport(httpTransport)
             .build();
     assertEquals(FIREBASE_DB_URL, firebaseOptions.getDatabaseUrl());
+    assertEquals(FIREBASE_STORAGE_BUCKET, firebaseOptions.getStorageBucket());
     assertSame(jsonFactory, firebaseOptions.getJsonFactory());
     assertSame(httpTransport, firebaseOptions.getHttpTransport());
     TestOnlyImplFirebaseAuthTrampolines.getCertificate(firebaseOptions.getCredential())
@@ -89,6 +93,8 @@ public class FirebaseOptionsTest {
             .build();
     assertNotNull(firebaseOptions.getJsonFactory());
     assertNotNull(firebaseOptions.getHttpTransport());
+    assertNull(firebaseOptions.getDatabaseUrl());
+    assertNull(firebaseOptions.getStorageBucket());
     TestOnlyImplFirebaseAuthTrampolines.getCertificate(firebaseOptions.getCredential())
         .addOnSuccessListener(
             new OnSuccessListener<GoogleCredential>() {
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
index 5cb3408..95f77f6 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
@@ -38,7 +38,10 @@ import com.google.firebase.auth.UserRecord.UpdateRequest;
 import com.google.firebase.tasks.Tasks;
 import com.google.firebase.testing.IntegrationTestUtils;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
+import java.util.Random;
 import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 import org.junit.BeforeClass;
@@ -107,14 +110,25 @@ public class FirebaseAuthIT {
     }
   }
 
+  private String randomPhoneNumber() {
+    Random random = new Random();
+    StringBuilder builder = new StringBuilder("+1");
+    for (int i = 0; i < 10; i++) {
+      builder.append(random.nextInt(10));
+    }
+    return builder.toString();
+  }
+
   @Test
   public void testCreateUserWithParams() throws Exception {
     String randomId = UUID.randomUUID().toString().replaceAll("-", "");
     String userEmail = ("test" + randomId.substring(0, 12) + "@example." + randomId.substring(12)
         + ".com").toLowerCase();
+    String phone = randomPhoneNumber();
     CreateRequest user = new CreateRequest()
         .setUid(randomId)
         .setEmail(userEmail)
+        .setPhoneNumber(phone)
         .setDisplayName("Random User")
         .setPhotoUrl("https://example.com/photo.png")
         .setEmailVerified(true)
@@ -125,10 +139,19 @@ public class FirebaseAuthIT {
       assertEquals(randomId, userRecord.getUid());
       assertEquals("Random User", userRecord.getDisplayName());
       assertEquals(userEmail, userRecord.getEmail());
+      assertEquals(phone, userRecord.getPhoneNumber());
       assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl());
       assertTrue(userRecord.isEmailVerified());
       assertFalse(userRecord.isDisabled());
 
+      assertEquals(2, userRecord.getProviderData().length);
+      List<String> providers = new ArrayList<>();
+      for (UserInfo provider : userRecord.getProviderData()) {
+        providers.add(provider.getProviderId());
+      }
+      assertTrue(providers.contains("password"));
+      assertTrue(providers.contains("phone"));
+
       checkRecreate(randomId);
     } finally {
       Tasks.await(auth.deleteUser(userRecord.getUid()));
@@ -157,19 +180,23 @@ public class FirebaseAuthIT {
     assertEquals(uid, userRecord.getUid());
     assertNull(userRecord.getDisplayName());
     assertNull(userRecord.getEmail());
+    assertNull(userRecord.getPhoneNumber());
     assertNull(userRecord.getPhotoUrl());
     assertFalse(userRecord.isEmailVerified());
     assertFalse(userRecord.isDisabled());
     assertTrue(userRecord.getUserMetadata().getCreationTimestamp() > 0);
     assertEquals(0, userRecord.getUserMetadata().getLastSignInTimestamp());
+    assertEquals(0, userRecord.getProviderData().length);
 
     // Update user
     String randomId = UUID.randomUUID().toString().replaceAll("-", "");
     String userEmail = ("test" + randomId.substring(0, 12) + "@example." + randomId.substring(12)
         + ".com").toLowerCase();
+    String phone = randomPhoneNumber();
     UpdateRequest request = userRecord.updateRequest()
         .setDisplayName("Updated Name")
         .setEmail(userEmail)
+        .setPhoneNumber(phone)
         .setPhotoUrl("https://example.com/photo.png")
         .setEmailVerified(true)
         .setPassword("secret");
@@ -177,9 +204,11 @@ public class FirebaseAuthIT {
     assertEquals(uid, userRecord.getUid());
     assertEquals("Updated Name", userRecord.getDisplayName());
     assertEquals(userEmail, userRecord.getEmail());
+    assertEquals(phone, userRecord.getPhoneNumber());
     assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl());
     assertTrue(userRecord.isEmailVerified());
     assertFalse(userRecord.isDisabled());
+    assertEquals(2, userRecord.getProviderData().length);
 
     // Get user by email
     userRecord = Tasks.await(auth.getUserByEmail(userRecord.getEmail()));
@@ -189,14 +218,17 @@ public class FirebaseAuthIT {
     request = userRecord.updateRequest()
         .setPhotoUrl(null)
         .setDisplayName(null)
+        .setPhoneNumber(null)
         .setDisabled(true);
     userRecord = Tasks.await(auth.updateUser(request));
     assertEquals(uid, userRecord.getUid());
     assertNull(userRecord.getDisplayName());
     assertEquals(userEmail, userRecord.getEmail());
+    assertNull(userRecord.getPhoneNumber());
     assertNull(userRecord.getPhotoUrl());
     assertTrue(userRecord.isEmailVerified());
     assertTrue(userRecord.isDisabled());
+    assertEquals(1, userRecord.getProviderData().length);
 
     // Delete user
     Tasks.await(auth.deleteUser(userRecord.getUid()));
diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
index 0a9bcee..894a0f9 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
@@ -18,6 +18,7 @@ package com.google.firebase.auth;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -88,6 +89,21 @@ public class FirebaseUserManagerTest {
     checkRequestHeaders(interceptor);
   }
 
+  @Test
+  public void testGetUserByPhoneNumber() throws Exception {
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+    response.setContent(TestUtils.loadResource("getUser.json"));
+    MockHttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(response)
+        .build();
+    FirebaseUserManager userManager = new FirebaseUserManager(gson, transport);
+    TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    userManager.setInterceptor(interceptor);
+    UserRecord userRecord = userManager.getUserByPhoneNumber("+1234567890", TEST_TOKEN);
+    checkUserRecord(userRecord);
+    checkRequestHeaders(interceptor);
+  }
+
   @Test
   public void testCreateUser() throws Exception {
     MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
@@ -182,14 +198,16 @@ public class FirebaseUserManagerTest {
         .setDisplayName("Display Name")
         .setPhotoUrl("http://test.com/example.png")
         .setEmail("test@example.com")
+        .setPhoneNumber("+1234567890")
         .setEmailVerified(true)
         .setPassword("secret")
         .getProperties();
-    assertEquals(6, map.size());
+    assertEquals(7, map.size());
     assertEquals("TestUid", map.get("localId"));
     assertEquals("Display Name", map.get("displayName"));
     assertEquals("http://test.com/example.png", map.get("photoUrl"));
     assertEquals("test@example.com", map.get("email"));
+    assertEquals("+1234567890", map.get("phoneNumber"));
     assertTrue((Boolean) map.get("emailVerified"));
     assertEquals("secret", map.get("password"));
   }
@@ -280,6 +298,31 @@ public class FirebaseUserManagerTest {
     }
   }
 
+  @Test
+  public void testInvalidPhoneNumber() {
+    CreateRequest user = new CreateRequest();
+    try {
+      user.setPhoneNumber(null);
+      fail("No error thrown for null phone number");
+    } catch (Exception ignore) {
+      // expected
+    }
+
+    try {
+      user.setPhoneNumber("");
+      fail("No error thrown for invalid phone number");
+    } catch (Exception ignore) {
+      // expected
+    }
+
+    try {
+      user.setPhoneNumber("not-a-phone");
+      fail("No error thrown for invalid phone number");
+    } catch (Exception ignore) {
+      // expected
+    }
+  }
+
   @Test
   public void testInvalidPassword() {
     CreateRequest user = new CreateRequest();
@@ -305,14 +348,16 @@ public class FirebaseUserManagerTest {
         .setDisplayName("Display Name")
         .setPhotoUrl("http://test.com/example.png")
         .setEmail("test@example.com")
+        .setPhoneNumber("+1234567890")
         .setEmailVerified(true)
         .setPassword("secret")
         .getProperties();
-    assertEquals(6, map.size());
+    assertEquals(7, map.size());
     assertEquals(update.getUid(), map.get("localId"));
     assertEquals("Display Name", map.get("displayName"));
     assertEquals("http://test.com/example.png", map.get("photoUrl"));
     assertEquals("test@example.com", map.get("email"));
+    assertEquals("+1234567890", map.get("phoneNumber"));
     assertTrue((Boolean) map.get("emailVerified"));
     assertEquals("secret", map.get("password"));
   }
@@ -333,6 +378,14 @@ public class FirebaseUserManagerTest {
     assertEquals(ImmutableList.of("PHOTO_URL"), map.get("deleteAttribute"));
   }
 
+  @Test
+  public void testDeletePhoneNumber() {
+    Map<String, Object> map = new UpdateRequest("test")
+        .setPhoneNumber(null)
+        .getProperties();
+    assertEquals(ImmutableList.of("phone"), map.get("deleteProvider"));
+  }
+
   @Test
   public void testInvalidUpdatePhotoUrl() {
     UpdateRequest update = new UpdateRequest("test");
@@ -376,6 +429,25 @@ public class FirebaseUserManagerTest {
     }
   }
 
+  @Test
+  public void testInvalidUpdatePhoneNumber() {
+    UpdateRequest update = new UpdateRequest("test");
+
+    try {
+      update.setPhoneNumber("");
+      fail("No error thrown for invalid phone number");
+    } catch (Exception ignore) {
+      // expected
+    }
+
+    try {
+      update.setPhoneNumber("not-a-phone");
+      fail("No error thrown for invalid phone number");
+    } catch (Exception ignore) {
+      // expected
+    }
+  }
+
   @Test
   public void testInvalidUpdatePassword() {
     UpdateRequest update = new UpdateRequest("test");
@@ -397,20 +469,28 @@ public class FirebaseUserManagerTest {
   private void checkUserRecord(UserRecord userRecord) {
     assertEquals("testuser", userRecord.getUid());
     assertEquals("testuser@example.com", userRecord.getEmail());
+    assertEquals("+1234567890", userRecord.getPhoneNumber());
     assertEquals("Test User", userRecord.getDisplayName());
     assertEquals("http://www.example.com/testuser/photo.png", userRecord.getPhotoUrl());
     assertEquals(1234567890, userRecord.getUserMetadata().getCreationTimestamp());
     assertEquals(0, userRecord.getUserMetadata().getLastSignInTimestamp());
-    assertEquals(1, userRecord.getProviderData().length);
+    assertEquals(2, userRecord.getProviderData().length);
     assertFalse(userRecord.isDisabled());
     assertTrue(userRecord.isEmailVerified());
 
     UserInfo provider = userRecord.getProviderData()[0];
     assertEquals("testuser@example.com", provider.getUid());
     assertEquals("testuser@example.com", provider.getEmail());
+    assertNull(provider.getPhoneNumber());
     assertEquals("Test User", provider.getDisplayName());
     assertEquals("http://www.example.com/testuser/photo.png", provider.getPhotoUrl());
     assertEquals("password", provider.getProviderId());
+
+    provider = userRecord.getProviderData()[1];
+    assertEquals("+1234567890", provider.getUid());
+    assertNull(provider.getEmail());
+    assertEquals("+1234567890", provider.getPhoneNumber());
+    assertEquals("phone", provider.getProviderId());
   }
 
   private void checkRequestHeaders(TestResponseInterceptor interceptor) {
diff --git a/src/test/java/com/google/firebase/cloud/StorageClientIT.java b/src/test/java/com/google/firebase/cloud/StorageClientIT.java
new file mode 100644
index 0000000..400fe26
--- /dev/null
+++ b/src/test/java/com/google/firebase/cloud/StorageClientIT.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2017 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.cloud;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.cloud.storage.Blob;
+import com.google.cloud.storage.Bucket;
+import com.google.firebase.testing.IntegrationTestUtils;
+import org.junit.Test;
+
+public class StorageClientIT {
+
+  @Test
+  public void testCloudStorageDefaultBucket() {
+    StorageClient storage = StorageClient.getInstance(IntegrationTestUtils.ensureDefaultApp());
+    testBucket(storage.bucket());
+  }
+
+  @Test
+  public void testCloudStorageCustomBucket() {
+    StorageClient storage = StorageClient.getInstance(IntegrationTestUtils.ensureDefaultApp());
+    testBucket(storage.bucket(IntegrationTestUtils.getStorageBucket()));
+  }
+
+  @Test
+  public void testCloudStorageNonExistingBucket() {
+    StorageClient storage = StorageClient.getInstance(IntegrationTestUtils.ensureDefaultApp());
+    try {
+      storage.bucket("non-existing");
+      fail("No error thrown for non-existing bucket");
+    } catch (IllegalArgumentException expected) {
+      // ignore
+    }
+  }
+
+  private void testBucket(Bucket bucket) {
+    assertEquals(IntegrationTestUtils.getStorageBucket(), bucket.getName());
+
+    String fileName = "data_" + System.currentTimeMillis() + ".txt";
+    bucket.create(fileName, "Hello World".getBytes(), "text/plain");
+
+    Blob blob = bucket.get(fileName);
+    byte[] content = blob.getContent();
+    assertEquals("Hello World", new String(content));
+
+    assertTrue(blob.delete());
+    assertNull(bucket.get(fileName));
+  }
+
+}
diff --git a/src/test/java/com/google/firebase/cloud/StorageClientTest.java b/src/test/java/com/google/firebase/cloud/StorageClientTest.java
new file mode 100644
index 0000000..3fa1663
--- /dev/null
+++ b/src/test/java/com/google/firebase/cloud/StorageClientTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2017 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.cloud;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
+import com.google.firebase.TestOnlyImplFirebaseTrampolines;
+import com.google.firebase.auth.FirebaseCredentials;
+import com.google.firebase.testing.ServiceAccount;
+import java.io.IOException;
+import org.junit.After;
+import org.junit.Test;
+
+public class StorageClientTest {
+
+  @After
+  public void tearDown() {
+    TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
+  }
+
+  @Test
+  public void testInvalidConfiguration() throws IOException {
+    FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder()
+        .setCredential(FirebaseCredentials.fromCertificate(ServiceAccount.EDITOR.asStream()))
+        .build());
+    try {
+      StorageClient.getInstance(app).bucket();
+      fail("No error thrown for invalid configuration");
+    } catch (IllegalArgumentException expected) {
+      // ignore
+    }
+  }
+
+  @Test
+  public void testInvalidBucketName() throws IOException {
+    FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder()
+        .setCredential(FirebaseCredentials.fromCertificate(ServiceAccount.EDITOR.asStream()))
+        .setStorageBucket("mock-bucket-name")
+        .build());
+    try {
+      StorageClient.getInstance(app).bucket(null);
+      fail("No error thrown for invalid configuration");
+    } catch (IllegalArgumentException expected) {
+      // ignore
+    }
+
+    try {
+      StorageClient.getInstance(app).bucket("");
+      fail("No error thrown for invalid configuration");
+    } catch (IllegalArgumentException expected) {
+      // ignore
+    }
+  }
+
+  @Test
+  public void testAppDelete() throws IOException {
+    FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder()
+        .setCredential(FirebaseCredentials.fromCertificate(ServiceAccount.EDITOR.asStream()))
+        .setStorageBucket("mock-bucket-name")
+        .build());
+
+    assertNotNull(StorageClient.getInstance(app));
+    app.delete();
+    try {
+      StorageClient.getInstance(app);
+      fail("No error thrown for deleted app");
+    } catch (IllegalStateException expected) {
+      // ignore
+    }
+  }
+}
diff --git a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java
index 296b718..369823d 100644
--- a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java
+++ b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java
@@ -78,6 +78,10 @@ public class IntegrationTestUtils {
     return "https://" + getProjectId() + ".firebaseio.com";
   }
 
+  public static String getStorageBucket() {
+    return getProjectId() + ".appspot.com";
+  }
+
   public static String getApiKey() {
     String apiKey = System.getProperty("firebase.it.apikey");
     checkArgument(!Strings.isNullOrEmpty(apiKey), "API key not specified. Set the "
@@ -98,6 +102,7 @@ public class IntegrationTestUtils {
       FirebaseOptions options =
           new FirebaseOptions.Builder()
               .setDatabaseUrl(getDatabaseUrl())
+              .setStorageBucket(getStorageBucket())
               .setCredential(TestUtils.getCertCredential(getServiceAccountCertificate()))
               .build();
       masterApp = FirebaseApp.initializeApp(options);
diff --git a/src/test/resources/getUser.json b/src/test/resources/getUser.json
index 8287ae1..14f5e7c 100644
--- a/src/test/resources/getUser.json
+++ b/src/test/resources/getUser.json
@@ -3,6 +3,7 @@
   "users" : [ {
     "localId" : "testuser",
     "email" : "testuser@example.com",
+    "phoneNumber" : "+1234567890",
     "emailVerified" : true,
     "displayName" : "Test User",
     "providerUserInfo" : [ {
@@ -12,6 +13,10 @@
       "federatedId" : "testuser@example.com",
       "email" : "testuser@example.com",
       "rawId" : "testuser@example.com"
+    }, {
+      "providerId" : "phone",
+      "phoneNumber" : "+1234567890",
+      "rawId" : "+1234567890"
     } ],
     "photoUrl" : "http://www.example.com/testuser/photo.png",
     "passwordHash" : "passwordhash",