Repository: firebase/firebase-admin-java
Pull request #391 (2020-04-16 21:19:39)

Add tenant-aware token generation and verification.
This pull request incorporates the tenant ID into the token generation and validation when using a tenant-aware client. This is part of the initiative to adding multi-tenancy support (see issue #332).


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 11012bf..c346c5b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -141,8 +141,17 @@ Authentication Admin` role at
 [Google Cloud Platform Console / IAM & admin](https://console.cloud.google.com/iam-admin). This is
 required to ensure that exported user records contain the password hashes of the user accounts.
 Also obtain the web API key of the project from the "Settings > General" page, and save it as
-`integration_apikey.txt` at the root of the codebase. Now run the following command to invoke the
-integration test suite:
+`integration_apikey.txt` at the root of the codebase.
+
+Some of the integration tests require an
+[Identity Platform](https://cloud.google.com/identity-platform/) project with multi-tenancy
+[enabled](https://cloud.google.com/identity-platform/docs/multi-tenancy-quickstart#enabling_multi-tenancy).
+An existing Firebase project can be upgraded to an Identity Platform project without losing any
+functionality via the
+[Identity Platform Marketplace Page](https://console.cloud.google.com/customer-identity). Note that
+charges may be incurred for active users beyond the Identity Platform free tier.
+
+Now run the following command to invoke the integration test suite:
 
 ```
 mvn verify
@@ -153,14 +162,14 @@ tests, specify the `-DskipUTs` flag.
 
 ### Generating API Docs
 
-Invoke the [Maven Javadoc plugin](https://maven.apache.org/plugins/maven-javadoc-plugin/) as 
+Invoke the [Maven Javadoc plugin](https://maven.apache.org/plugins/maven-javadoc-plugin/) as
 follows to generate API docs for all packages in the codebase:
 
 ```
 mvn javadoc:javadoc
 ```
 
-This will generate the API docs, and place them in the `target/site/apidocs` directory. 
+This will generate the API docs, and place them in the `target/site/apidocs` directory.
 
 To generate API docs for only the public APIs (i.e. ones that are not marked with `@hide` tags),
 you need to trigger the `devsite-apidocs` Maven profile. This profile uses Maven Javadoc plugin
diff --git a/pom.xml b/pom.xml
index 2bac82b..aa8d3c8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -321,13 +321,6 @@
                 <configuration>
                     <source>1.7</source>
                     <target>1.7</target>
-                    <annotationProcessorPaths>
-                        <path>
-                            <groupId>com.google.auto.value</groupId>
-                            <artifactId>auto-value</artifactId>
-                            <version>1.7</version>
-                        </path>
-                    </annotationProcessorPaths>
                 </configuration>
             </plugin>
             <plugin>
@@ -433,11 +426,6 @@
         </dependency>
 
         <!-- Utilities -->
-        <dependency>
-            <groupId>com.google.auto.value</groupId>
-            <artifactId>auto-value-annotations</artifactId>
-            <version>1.7</version>
-        </dependency>
         <dependency>
             <groupId>com.google.guava</groupId>
             <artifactId>guava</artifactId>
diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
index 0c20ec6..396d810 100644
--- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
@@ -1092,7 +1092,7 @@ public abstract class AbstractFirebaseAuth {
       destroyed.set(true);
     }
   }
-  
+
   /** Performs any additional required clean up. */
   protected abstract void doDestroy();
 
diff --git a/src/main/java/com/google/firebase/auth/FirebaseToken.java b/src/main/java/com/google/firebase/auth/FirebaseToken.java
index 0cd09a5..835fa41 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseToken.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseToken.java
@@ -44,7 +44,11 @@ public final class FirebaseToken {
 
   /** Returns the tenant ID for the this token. */
   public String getTenantId() {
-    return (String) claims.get("tenant_id");
+    Map<String, Object> firebase = (Map<String, Object>) claims.get("firebase");
+    if (firebase == null) {
+      return null;
+    }
+    return (String) firebase.get("tenant");
   }
 
   /** Returns the Issuer for the this token. */
diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java
index dbb5628..e0105e9 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java
@@ -30,6 +30,7 @@ import com.google.firebase.FirebaseOptions;
 import com.google.firebase.ImplFirebaseTrampolines;
 import com.google.firebase.auth.internal.CryptoSigners;
 import com.google.firebase.auth.internal.FirebaseTokenFactory;
+import com.google.firebase.internal.Nullable;
 
 import java.io.IOException;
 
@@ -52,11 +53,17 @@ final class FirebaseTokenUtils {
   private FirebaseTokenUtils() { }
 
   static FirebaseTokenFactory createTokenFactory(FirebaseApp firebaseApp, Clock clock) {
+    return createTokenFactory(firebaseApp, clock, null);
+  }
+
+  static FirebaseTokenFactory createTokenFactory(
+      FirebaseApp firebaseApp, Clock clock, @Nullable String tenantId) {
     try {
       return new FirebaseTokenFactory(
           firebaseApp.getOptions().getJsonFactory(),
           clock,
-          CryptoSigners.getCryptoSigner(firebaseApp));
+          CryptoSigners.getCryptoSigner(firebaseApp),
+          tenantId);
     } catch (IOException e) {
       throw new IllegalStateException(
           "Failed to initialize FirebaseTokenFactory. Make sure to initialize the SDK "
@@ -68,6 +75,11 @@ final class FirebaseTokenUtils {
   }
 
   static FirebaseTokenVerifierImpl createIdTokenVerifier(FirebaseApp app, Clock clock) {
+    return createIdTokenVerifier(app, clock, null);
+  }
+
+  static FirebaseTokenVerifierImpl createIdTokenVerifier(
+      FirebaseApp app, Clock clock, @Nullable String tenantId) {
     String projectId = ImplFirebaseTrampolines.getProjectId(app);
     checkState(!Strings.isNullOrEmpty(projectId),
         "Must initialize FirebaseApp with a project ID to call verifyIdToken()");
@@ -82,6 +94,7 @@ final class FirebaseTokenUtils {
         .setJsonFactory(app.getOptions().getJsonFactory())
         .setPublicKeysManager(publicKeysManager)
         .setIdTokenVerifier(idTokenVerifier)
+        .setTenantId(tenantId)
         .build();
   }
 
diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java
index c164173..660506c 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java
@@ -28,6 +28,7 @@ import com.google.api.client.json.webtoken.JsonWebSignature.Header;
 import com.google.api.client.util.ArrayMap;
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
+import com.google.firebase.internal.Nullable;
 import java.io.IOException;
 import java.math.BigDecimal;
 import java.security.GeneralSecurityException;
@@ -53,6 +54,7 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
   private final String shortName;
   private final String articledShortName;
   private final String docUrl;
+  private final String tenantId;
 
   private FirebaseTokenVerifierImpl(Builder builder) {
     this.jsonFactory = checkNotNull(builder.jsonFactory);
@@ -65,6 +67,7 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
     this.shortName = builder.shortName;
     this.articledShortName = prefixWithIndefiniteArticle(this.shortName);
     this.docUrl = builder.docUrl;
+    this.tenantId = Strings.nullToEmpty(builder.tenantId);
   }
 
   /**
@@ -90,7 +93,9 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
     IdToken idToken = parse(token);
     checkContents(idToken);
     checkSignature(idToken);
-    return new FirebaseToken(idToken.getPayload());
+    FirebaseToken firebaseToken = new FirebaseToken(idToken.getPayload());
+    checkTenantId(firebaseToken);
+    return firebaseToken;
   }
 
   GooglePublicKeysManager getPublicKeysManager() {
@@ -278,6 +283,18 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
     return false;
   }
 
+  private void checkTenantId(final FirebaseToken firebaseToken) throws FirebaseAuthException {
+    String tokenTenantId = Strings.nullToEmpty(firebaseToken.getTenantId());
+    if (!this.tenantId.equals(tokenTenantId)) {
+      throw new FirebaseAuthException(
+          FirebaseUserManager.TENANT_ID_MISMATCH_ERROR,
+          String.format(
+            "The tenant ID ('%s') of the token did not match the expected ('%s') value",
+            tokenTenantId,
+            tenantId));
+    }
+  }
+
   static Builder builder() {
     return new Builder();
   }
@@ -290,6 +307,7 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
     private String shortName;
     private IdTokenVerifier idTokenVerifier;
     private String docUrl;
+    private String tenantId;
 
     private Builder() { }
 
@@ -323,6 +341,11 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
       return this;
     }
 
+    Builder setTenantId(@Nullable String tenantId) {
+      this.tenantId = tenantId;
+      return this;
+    }
+
     FirebaseTokenVerifierImpl build() {
       return new FirebaseTokenVerifierImpl(this);
     }
diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
index c364242..12b4119 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
@@ -33,6 +33,7 @@ import com.google.api.client.json.JsonFactory;
 import com.google.api.client.json.JsonObjectParser;
 import com.google.api.client.util.Key;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -65,6 +66,7 @@ import java.util.Map;
  */
 class FirebaseUserManager {
 
+  static final String TENANT_ID_MISMATCH_ERROR = "tenant-id-mismatch";
   static final String TENANT_NOT_FOUND_ERROR = "tenant-not-found";
   static final String USER_NOT_FOUND_ERROR = "user-not-found";
   static final String INTERNAL_ERROR = "internal-error";
@@ -84,6 +86,7 @@ class FirebaseUserManager {
       .put("INVALID_PHONE_NUMBER", "invalid-phone-number")
       .put("PHONE_NUMBER_EXISTS", "phone-number-already-exists")
       .put("PROJECT_NOT_FOUND", "project-not-found")
+      .put("TENANT_ID_MISMATCH", TENANT_ID_MISMATCH_ERROR)
       .put("TENANT_NOT_FOUND", TENANT_NOT_FOUND_ERROR)
       .put("USER_NOT_FOUND", USER_NOT_FOUND_ERROR)
       .put("WEAK_PASSWORD", "invalid-password")
@@ -271,7 +274,7 @@ class FirebaseUserManager {
     // since we do not currently generate masks for any properties with nested values. When it
     // comes time to implement this, we can check if a property has nested properties by checking
     // if it is an instance of the Map class.
-    return String.join(",", ImmutableSortedSet.copyOf(properties.keySet()));
+    return Joiner.on(",").join(ImmutableSortedSet.copyOf(properties.keySet()));
   }
 
   void deleteTenant(String tenantId) throws FirebaseAuthException {
diff --git a/src/main/java/com/google/firebase/auth/TenantAwareFirebaseAuth.java b/src/main/java/com/google/firebase/auth/TenantAwareFirebaseAuth.java
index 062b071..07de94e 100644
--- a/src/main/java/com/google/firebase/auth/TenantAwareFirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/TenantAwareFirebaseAuth.java
@@ -41,22 +41,20 @@ public class TenantAwareFirebaseAuth extends AbstractFirebaseAuth {
   }
 
   private static Builder builderFromAppAndTenantId(final FirebaseApp app, final String tenantId) {
-    // TODO(micahstairs): Incorporate tenant ID into token generation as well as ID token and
-    // session cookie verification.
     return AbstractFirebaseAuth.builder()
         .setFirebaseApp(app)
         .setTokenFactory(
             new Supplier<FirebaseTokenFactory>() {
               @Override
               public FirebaseTokenFactory get() {
-                return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM);
+                return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM, tenantId);
               }
             })
         .setIdTokenVerifier(
             new Supplier<FirebaseTokenVerifier>() {
               @Override
               public FirebaseTokenVerifier get() {
-                return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM);
+                return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM, tenantId);
               }
             })
         .setCookieVerifier(
diff --git a/src/main/java/com/google/firebase/auth/internal/FirebaseCustomAuthToken.java b/src/main/java/com/google/firebase/auth/internal/FirebaseCustomAuthToken.java
index e675764..2fe0b18 100644
--- a/src/main/java/com/google/firebase/auth/internal/FirebaseCustomAuthToken.java
+++ b/src/main/java/com/google/firebase/auth/internal/FirebaseCustomAuthToken.java
@@ -22,6 +22,7 @@ import com.google.api.client.json.JsonFactory;
 import com.google.api.client.json.webtoken.JsonWebSignature;
 import com.google.api.client.util.Key;
 import com.google.firebase.auth.FirebaseToken;
+import com.google.firebase.internal.Nullable;
 
 import java.io.IOException;
 
@@ -77,6 +78,9 @@ public final class FirebaseCustomAuthToken extends IdToken {
     @Key("claims")
     private GenericJson developerClaims;
 
+    @Key("tenant_id")
+    private String tenantId;
+
     public final String getUid() {
       return uid;
     }
@@ -95,6 +99,15 @@ public final class FirebaseCustomAuthToken extends IdToken {
       return this;
     }
 
+    public final String getTenantId() {
+      return tenantId;
+    }
+
+    public Payload setTenantId(String tenantId) {
+      this.tenantId = tenantId;
+      return this;
+    }
+
     @Override
     public Payload setIssuer(String issuer) {
       return (Payload) super.setIssuer(issuer);
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 95d3131..778911d 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,9 @@ import com.google.api.client.json.webtoken.JsonWebSignature;
 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.internal.Nullable;
+
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Map;
@@ -41,11 +42,18 @@ public class FirebaseTokenFactory {
   private final JsonFactory jsonFactory;
   private final Clock clock;
   private final CryptoSigner signer;
+  private final String tenantId;
 
   public FirebaseTokenFactory(JsonFactory jsonFactory, Clock clock, CryptoSigner signer) {
+    this(jsonFactory, clock, signer, null);
+  }
+
+  public FirebaseTokenFactory(
+      JsonFactory jsonFactory, Clock clock, CryptoSigner signer, @Nullable String tenantId) {
     this.jsonFactory = checkNotNull(jsonFactory);
     this.clock = checkNotNull(clock);
     this.signer = checkNotNull(signer);
+    this.tenantId = tenantId;
   }
 
   String createSignedCustomAuthTokenForUser(String uid) throws IOException {
@@ -68,6 +76,9 @@ public class FirebaseTokenFactory {
             .setAudience(FirebaseCustomAuthToken.FIREBASE_AUDIENCE)
             .setIssuedAtTimeSeconds(issuedAt)
             .setExpirationTimeSeconds(issuedAt + FirebaseCustomAuthToken.TOKEN_DURATION_SECONDS);
+    if (!Strings.isNullOrEmpty(tenantId)) {
+      payload.setTenantId(tenantId);
+    }
 
     if (developerClaims != null) {
       Collection<String> reservedNames = payload.getClassInfo().getNames();


Test patch:
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
index c45c228..bb98389 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
@@ -47,6 +47,7 @@ import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.ImplFirebaseTrampolines;
 import com.google.firebase.auth.hash.Scrypt;
+import com.google.firebase.internal.Nullable;
 import com.google.firebase.testing.IntegrationTestUtils;
 import java.io.IOException;
 import java.net.URLDecoder;
@@ -705,6 +706,58 @@ public class FirebaseAuthIT {
     }
   }
 
+  @Test
+  public void testTenantAwareCustomToken() throws Exception {
+    // Create tenant to use.
+    TenantManager tenantManager = auth.getTenantManager();
+    Tenant.CreateRequest tenantCreateRequest =
+        new Tenant.CreateRequest().setDisplayName("DisplayName");
+    String tenantId = tenantManager.createTenant(tenantCreateRequest).getTenantId();
+
+    try {
+      // Create and decode a token with a tenant-aware client.
+      TenantAwareFirebaseAuth tenantAwareAuth = auth.getTenantManager().getAuthForTenant(tenantId);
+      String customToken = tenantAwareAuth.createCustomTokenAsync("user1").get();
+      String idToken = signInWithCustomToken(customToken, tenantId);
+      FirebaseToken decoded = tenantAwareAuth.verifyIdTokenAsync(idToken).get();
+      assertEquals("user1", decoded.getUid());
+      assertEquals(tenantId, decoded.getTenantId());
+    } finally {
+      // Delete tenant.
+      tenantManager.deleteTenantAsync(tenantId).get();
+    }
+  }
+
+  @Test
+  public void testVerifyTokenWithWrongTenantAwareClient() throws Exception {
+    // Create tenant to use.
+    TenantManager tenantManager = auth.getTenantManager();
+    Tenant.CreateRequest tenantCreateRequest =
+        new Tenant.CreateRequest().setDisplayName("DisplayName");
+    String tenantId = tenantManager.createTenant(tenantCreateRequest).getTenantId();
+
+    // Create tenant-aware clients.
+    TenantAwareFirebaseAuth tenantAwareAuth1 = auth.getTenantManager().getAuthForTenant(tenantId);
+    TenantAwareFirebaseAuth tenantAwareAuth2 = auth.getTenantManager().getAuthForTenant("OTHER");
+
+    try {
+      // Create a token with one client and decode with the other.
+      String customToken = tenantAwareAuth1.createCustomTokenAsync("user").get();
+      String idToken = signInWithCustomToken(customToken, tenantId);
+      try {
+        tenantAwareAuth2.verifyIdTokenAsync(idToken).get();
+        fail("No error thrown for verifying a token with the wrong tenant-aware client");
+      } catch (ExecutionException e) {
+        assertTrue(e.getCause() instanceof FirebaseAuthException);
+        assertEquals(FirebaseUserManager.TENANT_ID_MISMATCH_ERROR,
+            ((FirebaseAuthException) e.getCause()).getErrorCode());
+      }
+    } finally {
+      // Delete tenant.
+      tenantManager.deleteTenantAsync(tenantId).get();
+    }
+  }
+
   @Test
   public void testVerifyIdToken() throws Exception {
     String customToken = auth.createCustomTokenAsync("user2").get();
@@ -931,12 +984,21 @@ public class FirebaseAuthIT {
   }
 
   private String signInWithCustomToken(String customToken) throws IOException {
-    GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key="
+    return signInWithCustomToken(customToken, null);
+  }
+
+  private String signInWithCustomToken(
+      String customToken, @Nullable String tenantId) throws IOException {
+    final GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key="
         + IntegrationTestUtils.getApiKey());
-    Map<String, Object> content = ImmutableMap.<String, Object>of(
-        "token", customToken, "returnSecureToken", true);
+    ImmutableMap.Builder<String, Object> content = ImmutableMap.<String, Object>builder();
+    content.put("token", customToken);
+    content.put("returnSecureToken", true);
+    if (tenantId != null) {
+      content.put("tenantId", tenantId);
+    }
     HttpRequest request = transport.createRequestFactory().buildPostRequest(url,
-        new JsonHttpContent(jsonFactory, content));
+        new JsonHttpContent(jsonFactory, content.build()));
     request.setParser(new JsonObjectParser(jsonFactory));
     HttpResponse response = request.execute();
     try {
diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java
index b10817a..8b2c126 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java
@@ -29,6 +29,8 @@ import com.google.api.client.testing.http.MockHttpTransport;
 import com.google.api.client.testing.http.MockLowLevelHttpResponse;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.firebase.auth.internal.FirebaseCustomAuthToken;
 import com.google.firebase.testing.ServiceAccount;
 import java.io.IOException;
 import java.util.concurrent.TimeUnit;
@@ -216,6 +218,50 @@ public class FirebaseTokenVerifierImplTest {
     tokenVerifier.verifyToken("not.a.jwt");
   }
 
+  @Test
+  public void testVerifyTokenDifferentTenantIds() throws Exception {
+    try {
+      fullyPopulatedBuilder()
+        .setTenantId("TENANT_1")
+        .build()
+        .verifyToken(createTokenWithTenantId("TENANT_2"));
+    } catch (FirebaseAuthException e) {
+      assertEquals(FirebaseUserManager.TENANT_ID_MISMATCH_ERROR, e.getErrorCode());
+      assertEquals(
+          "The tenant ID ('TENANT_2') of the token did not match the expected ('TENANT_1') value",
+          e.getMessage());
+    }
+  }
+
+  @Test
+  public void testVerifyTokenMissingTenantId() throws Exception {
+    try {
+      fullyPopulatedBuilder()
+        .setTenantId("TENANT_ID")
+        .build()
+        .verifyToken(tokenFactory.createToken());
+    } catch (FirebaseAuthException e) {
+      assertEquals(FirebaseUserManager.TENANT_ID_MISMATCH_ERROR, e.getErrorCode());
+      assertEquals(
+          "The tenant ID ('') of the token did not match the expected ('TENANT_ID') value",
+          e.getMessage());
+    }
+  }
+
+  @Test
+  public void testVerifyTokenUnexpectedTenantId() throws Exception {
+    try {
+      fullyPopulatedBuilder()
+        .build()
+        .verifyToken(createTokenWithTenantId("TENANT_ID"));
+    } catch (FirebaseAuthException e) {
+      assertEquals(FirebaseUserManager.TENANT_ID_MISMATCH_ERROR, e.getErrorCode());
+      assertEquals(
+          "The tenant ID ('TENANT_ID') of the token did not match the expected ('') value",
+          e.getMessage());
+    }
+  }
+
   @Test(expected = NullPointerException.class)
   public void testBuilderNoPublicKeysManager() {
     fullyPopulatedBuilder().setPublicKeysManager(null).build();
@@ -337,4 +383,10 @@ public class FirebaseTokenVerifierImplTest {
     payload.setExpirationTimeSeconds(expirationSeconds);
     return tokenFactory.createToken(payload);
   }
+
+  private String createTokenWithTenantId(String tenantId) {
+    Payload payload = tokenFactory.createTokenPayload();
+    payload.set("firebase", ImmutableMap.of("tenant", tenantId));
+    return tokenFactory.createToken(payload);
+  }
 }
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 71b95be..1c85cee 100644
--- a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java
+++ b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java
@@ -18,6 +18,7 @@ package com.google.firebase.auth.internal;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import com.google.api.client.json.GenericJson;
@@ -45,6 +46,7 @@ public class FirebaseTokenFactoryTest {
   private static final String USER_ID = "fuber";
   private static final GenericJson EXTRA_CLAIMS = new GenericJson();
   private static final String ISSUER = "test-484@mg-test-1210.iam.gserviceaccount.com";
+  private static final String TENANT_ID = "tenant-id";
 
   static {
     EXTRA_CLAIMS.set("one", 2).set("three", "four").setFactory(FACTORY);
@@ -71,6 +73,7 @@ public class FirebaseTokenFactoryTest {
     assertEquals(USER_ID, signedJwt.getPayload().getUid());
     assertEquals(2L, signedJwt.getPayload().getIssuedAtTimeSeconds().longValue());
     assertTrue(TestUtils.verifySignature(signedJwt, ImmutableList.of(keys.getPublic())));
+    assertNull(signedJwt.getPayload().getTenantId());
 
     jwt = tokenFactory.createSignedCustomAuthTokenForUser(USER_ID);
     signedJwt = FirebaseCustomAuthToken.parse(FACTORY, jwt);
@@ -80,6 +83,23 @@ public class FirebaseTokenFactoryTest {
     assertEquals(USER_ID, signedJwt.getPayload().getUid());
     assertEquals(2L, signedJwt.getPayload().getIssuedAtTimeSeconds().longValue());
     assertTrue(TestUtils.verifySignature(signedJwt, ImmutableList.of(keys.getPublic())));
+    assertNull(signedJwt.getPayload().getTenantId());
+  }
+
+  @Test
+  public void tokenWithTenantId() throws Exception {
+    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+    keyGen.initialize(512);
+    KeyPair keys = keyGen.genKeyPair();
+    FixedClock clock = new FixedClock(2002L);
+    CryptoSigner cryptoSigner = new TestCryptoSigner(keys.getPrivate());
+    FirebaseTokenFactory tokenFactory =
+        new FirebaseTokenFactory(FACTORY, clock, cryptoSigner, TENANT_ID);
+
+    String jwt = tokenFactory.createSignedCustomAuthTokenForUser(USER_ID);
+    FirebaseCustomAuthToken signedJwt = FirebaseCustomAuthToken.parse(FACTORY, jwt);
+
+    assertEquals(TENANT_ID, signedJwt.getPayload().getTenantId());
   }
 
   @Test