Skip to content

Commit b97bb8a

Browse files
authored
Add tenant-aware token generation and verification. (#391)
This incorporates the tenant ID into the token generation and validation when using a tenant-aware client. This is part of the initiative to add multi-tenancy support (see issue #332).
1 parent ece0030 commit b97bb8a

11 files changed

+211
-13
lines changed

src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1092,7 +1092,7 @@ final void destroy() {
10921092
destroyed.set(true);
10931093
}
10941094
}
1095-
1095+
10961096
/** Performs any additional required clean up. */
10971097
protected abstract void doDestroy();
10981098

src/main/java/com/google/firebase/auth/FirebaseToken.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ public String getUid() {
4444

4545
/** Returns the tenant ID for the this token. */
4646
public String getTenantId() {
47-
return (String) claims.get("tenant_id");
47+
Map<String, Object> firebase = (Map<String, Object>) claims.get("firebase");
48+
if (firebase == null) {
49+
return null;
50+
}
51+
return (String) firebase.get("tenant");
4852
}
4953

5054
/** Returns the Issuer for the this token. */

src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.google.firebase.ImplFirebaseTrampolines;
3131
import com.google.firebase.auth.internal.CryptoSigners;
3232
import com.google.firebase.auth.internal.FirebaseTokenFactory;
33+
import com.google.firebase.internal.Nullable;
3334

3435
import java.io.IOException;
3536

@@ -52,11 +53,17 @@ final class FirebaseTokenUtils {
5253
private FirebaseTokenUtils() { }
5354

5455
static FirebaseTokenFactory createTokenFactory(FirebaseApp firebaseApp, Clock clock) {
56+
return createTokenFactory(firebaseApp, clock, null);
57+
}
58+
59+
static FirebaseTokenFactory createTokenFactory(
60+
FirebaseApp firebaseApp, Clock clock, @Nullable String tenantId) {
5561
try {
5662
return new FirebaseTokenFactory(
5763
firebaseApp.getOptions().getJsonFactory(),
5864
clock,
59-
CryptoSigners.getCryptoSigner(firebaseApp));
65+
CryptoSigners.getCryptoSigner(firebaseApp),
66+
tenantId);
6067
} catch (IOException e) {
6168
throw new IllegalStateException(
6269
"Failed to initialize FirebaseTokenFactory. Make sure to initialize the SDK "
@@ -68,6 +75,11 @@ static FirebaseTokenFactory createTokenFactory(FirebaseApp firebaseApp, Clock cl
6875
}
6976

7077
static FirebaseTokenVerifierImpl createIdTokenVerifier(FirebaseApp app, Clock clock) {
78+
return createIdTokenVerifier(app, clock, null);
79+
}
80+
81+
static FirebaseTokenVerifierImpl createIdTokenVerifier(
82+
FirebaseApp app, Clock clock, @Nullable String tenantId) {
7183
String projectId = ImplFirebaseTrampolines.getProjectId(app);
7284
checkState(!Strings.isNullOrEmpty(projectId),
7385
"Must initialize FirebaseApp with a project ID to call verifyIdToken()");
@@ -82,6 +94,7 @@ static FirebaseTokenVerifierImpl createIdTokenVerifier(FirebaseApp app, Clock cl
8294
.setJsonFactory(app.getOptions().getJsonFactory())
8395
.setPublicKeysManager(publicKeysManager)
8496
.setIdTokenVerifier(idTokenVerifier)
97+
.setTenantId(tenantId)
8598
.build();
8699
}
87100

src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.google.api.client.util.ArrayMap;
2929
import com.google.common.base.Joiner;
3030
import com.google.common.base.Strings;
31+
import com.google.firebase.internal.Nullable;
3132
import java.io.IOException;
3233
import java.math.BigDecimal;
3334
import java.security.GeneralSecurityException;
@@ -53,6 +54,7 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
5354
private final String shortName;
5455
private final String articledShortName;
5556
private final String docUrl;
57+
private final String tenantId;
5658

5759
private FirebaseTokenVerifierImpl(Builder builder) {
5860
this.jsonFactory = checkNotNull(builder.jsonFactory);
@@ -65,6 +67,7 @@ private FirebaseTokenVerifierImpl(Builder builder) {
6567
this.shortName = builder.shortName;
6668
this.articledShortName = prefixWithIndefiniteArticle(this.shortName);
6769
this.docUrl = builder.docUrl;
70+
this.tenantId = Strings.nullToEmpty(builder.tenantId);
6871
}
6972

7073
/**
@@ -90,7 +93,9 @@ public FirebaseToken verifyToken(String token) throws FirebaseAuthException {
9093
IdToken idToken = parse(token);
9194
checkContents(idToken);
9295
checkSignature(idToken);
93-
return new FirebaseToken(idToken.getPayload());
96+
FirebaseToken firebaseToken = new FirebaseToken(idToken.getPayload());
97+
checkTenantId(firebaseToken);
98+
return firebaseToken;
9499
}
95100

96101
GooglePublicKeysManager getPublicKeysManager() {
@@ -278,6 +283,18 @@ private boolean containsLegacyUidField(IdToken.Payload payload) {
278283
return false;
279284
}
280285

286+
private void checkTenantId(final FirebaseToken firebaseToken) throws FirebaseAuthException {
287+
String tokenTenantId = Strings.nullToEmpty(firebaseToken.getTenantId());
288+
if (!this.tenantId.equals(tokenTenantId)) {
289+
throw new FirebaseAuthException(
290+
FirebaseUserManager.TENANT_ID_MISMATCH_ERROR,
291+
String.format(
292+
"The tenant ID ('%s') of the token did not match the expected ('%s') value",
293+
tokenTenantId,
294+
tenantId));
295+
}
296+
}
297+
281298
static Builder builder() {
282299
return new Builder();
283300
}
@@ -290,6 +307,7 @@ static final class Builder {
290307
private String shortName;
291308
private IdTokenVerifier idTokenVerifier;
292309
private String docUrl;
310+
private String tenantId;
293311

294312
private Builder() { }
295313

@@ -323,6 +341,11 @@ Builder setDocUrl(String docUrl) {
323341
return this;
324342
}
325343

344+
Builder setTenantId(@Nullable String tenantId) {
345+
this.tenantId = tenantId;
346+
return this;
347+
}
348+
326349
FirebaseTokenVerifierImpl build() {
327350
return new FirebaseTokenVerifierImpl(this);
328351
}

src/main/java/com/google/firebase/auth/FirebaseUserManager.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
*/
6767
class FirebaseUserManager {
6868

69+
static final String TENANT_ID_MISMATCH_ERROR = "tenant-id-mismatch";
6970
static final String TENANT_NOT_FOUND_ERROR = "tenant-not-found";
7071
static final String USER_NOT_FOUND_ERROR = "user-not-found";
7172
static final String INTERNAL_ERROR = "internal-error";
@@ -85,6 +86,7 @@ class FirebaseUserManager {
8586
.put("INVALID_PHONE_NUMBER", "invalid-phone-number")
8687
.put("PHONE_NUMBER_EXISTS", "phone-number-already-exists")
8788
.put("PROJECT_NOT_FOUND", "project-not-found")
89+
.put("TENANT_ID_MISMATCH", TENANT_ID_MISMATCH_ERROR)
8890
.put("TENANT_NOT_FOUND", TENANT_NOT_FOUND_ERROR)
8991
.put("USER_NOT_FOUND", USER_NOT_FOUND_ERROR)
9092
.put("WEAK_PASSWORD", "invalid-password")

src/main/java/com/google/firebase/auth/TenantAwareFirebaseAuth.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,20 @@ public class TenantAwareFirebaseAuth extends AbstractFirebaseAuth {
4141
}
4242

4343
private static Builder builderFromAppAndTenantId(final FirebaseApp app, final String tenantId) {
44-
// TODO(micahstairs): Incorporate tenant ID into token generation as well as ID token and
45-
// session cookie verification.
4644
return AbstractFirebaseAuth.builder()
4745
.setFirebaseApp(app)
4846
.setTokenFactory(
4947
new Supplier<FirebaseTokenFactory>() {
5048
@Override
5149
public FirebaseTokenFactory get() {
52-
return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM);
50+
return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM, tenantId);
5351
}
5452
})
5553
.setIdTokenVerifier(
5654
new Supplier<FirebaseTokenVerifier>() {
5755
@Override
5856
public FirebaseTokenVerifier get() {
59-
return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM);
57+
return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM, tenantId);
6058
}
6159
})
6260
.setCookieVerifier(

src/main/java/com/google/firebase/auth/internal/FirebaseCustomAuthToken.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.api.client.json.webtoken.JsonWebSignature;
2323
import com.google.api.client.util.Key;
2424
import com.google.firebase.auth.FirebaseToken;
25+
import com.google.firebase.internal.Nullable;
2526

2627
import java.io.IOException;
2728

@@ -77,6 +78,9 @@ public static class Payload extends IdToken.Payload {
7778
@Key("claims")
7879
private GenericJson developerClaims;
7980

81+
@Key("tenant_id")
82+
private String tenantId;
83+
8084
public final String getUid() {
8185
return uid;
8286
}
@@ -95,6 +99,15 @@ public Payload setDeveloperClaims(GenericJson developerClaims) {
9599
return this;
96100
}
97101

102+
public final String getTenantId() {
103+
return tenantId;
104+
}
105+
106+
public Payload setTenantId(String tenantId) {
107+
this.tenantId = tenantId;
108+
return this;
109+
}
110+
98111
@Override
99112
public Payload setIssuer(String issuer) {
100113
return (Payload) super.setIssuer(issuer);

src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@
2525
import com.google.api.client.util.Base64;
2626
import com.google.api.client.util.Clock;
2727
import com.google.api.client.util.StringUtils;
28-
2928
import com.google.common.base.Strings;
29+
import com.google.firebase.internal.Nullable;
30+
3031
import java.io.IOException;
3132
import java.util.Collection;
3233
import java.util.Map;
@@ -41,11 +42,18 @@ public class FirebaseTokenFactory {
4142
private final JsonFactory jsonFactory;
4243
private final Clock clock;
4344
private final CryptoSigner signer;
45+
private final String tenantId;
4446

4547
public FirebaseTokenFactory(JsonFactory jsonFactory, Clock clock, CryptoSigner signer) {
48+
this(jsonFactory, clock, signer, null);
49+
}
50+
51+
public FirebaseTokenFactory(
52+
JsonFactory jsonFactory, Clock clock, CryptoSigner signer, @Nullable String tenantId) {
4653
this.jsonFactory = checkNotNull(jsonFactory);
4754
this.clock = checkNotNull(clock);
4855
this.signer = checkNotNull(signer);
56+
this.tenantId = tenantId;
4957
}
5058

5159
String createSignedCustomAuthTokenForUser(String uid) throws IOException {
@@ -68,6 +76,9 @@ public String createSignedCustomAuthTokenForUser(
6876
.setAudience(FirebaseCustomAuthToken.FIREBASE_AUDIENCE)
6977
.setIssuedAtTimeSeconds(issuedAt)
7078
.setExpirationTimeSeconds(issuedAt + FirebaseCustomAuthToken.TOKEN_DURATION_SECONDS);
79+
if (!Strings.isNullOrEmpty(tenantId)) {
80+
payload.setTenantId(tenantId);
81+
}
7182

7283
if (developerClaims != null) {
7384
Collection<String> reservedNames = payload.getClassInfo().getNames();

src/test/java/com/google/firebase/auth/FirebaseAuthIT.java

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import com.google.firebase.FirebaseOptions;
4848
import com.google.firebase.ImplFirebaseTrampolines;
4949
import com.google.firebase.auth.hash.Scrypt;
50+
import com.google.firebase.internal.Nullable;
5051
import com.google.firebase.testing.IntegrationTestUtils;
5152
import java.io.IOException;
5253
import java.net.URLDecoder;
@@ -705,6 +706,58 @@ public void testCustomTokenWithIAM() throws Exception {
705706
}
706707
}
707708

709+
@Test
710+
public void testTenantAwareCustomToken() throws Exception {
711+
// Create tenant to use.
712+
TenantManager tenantManager = auth.getTenantManager();
713+
Tenant.CreateRequest tenantCreateRequest =
714+
new Tenant.CreateRequest().setDisplayName("DisplayName");
715+
String tenantId = tenantManager.createTenant(tenantCreateRequest).getTenantId();
716+
717+
try {
718+
// Create and decode a token with a tenant-aware client.
719+
TenantAwareFirebaseAuth tenantAwareAuth = auth.getTenantManager().getAuthForTenant(tenantId);
720+
String customToken = tenantAwareAuth.createCustomTokenAsync("user1").get();
721+
String idToken = signInWithCustomToken(customToken, tenantId);
722+
FirebaseToken decoded = tenantAwareAuth.verifyIdTokenAsync(idToken).get();
723+
assertEquals("user1", decoded.getUid());
724+
assertEquals(tenantId, decoded.getTenantId());
725+
} finally {
726+
// Delete tenant.
727+
tenantManager.deleteTenantAsync(tenantId).get();
728+
}
729+
}
730+
731+
@Test
732+
public void testVerifyTokenWithWrongTenantAwareClient() throws Exception {
733+
// Create tenant to use.
734+
TenantManager tenantManager = auth.getTenantManager();
735+
Tenant.CreateRequest tenantCreateRequest =
736+
new Tenant.CreateRequest().setDisplayName("DisplayName");
737+
String tenantId = tenantManager.createTenant(tenantCreateRequest).getTenantId();
738+
739+
// Create tenant-aware clients.
740+
TenantAwareFirebaseAuth tenantAwareAuth1 = auth.getTenantManager().getAuthForTenant(tenantId);
741+
TenantAwareFirebaseAuth tenantAwareAuth2 = auth.getTenantManager().getAuthForTenant("OTHER");
742+
743+
try {
744+
// Create a token with one client and decode with the other.
745+
String customToken = tenantAwareAuth1.createCustomTokenAsync("user").get();
746+
String idToken = signInWithCustomToken(customToken, tenantId);
747+
try {
748+
tenantAwareAuth2.verifyIdTokenAsync(idToken).get();
749+
fail("No error thrown for verifying a token with the wrong tenant-aware client");
750+
} catch (ExecutionException e) {
751+
assertTrue(e.getCause() instanceof FirebaseAuthException);
752+
assertEquals(FirebaseUserManager.TENANT_ID_MISMATCH_ERROR,
753+
((FirebaseAuthException) e.getCause()).getErrorCode());
754+
}
755+
} finally {
756+
// Delete tenant.
757+
tenantManager.deleteTenantAsync(tenantId).get();
758+
}
759+
}
760+
708761
@Test
709762
public void testVerifyIdToken() throws Exception {
710763
String customToken = auth.createCustomTokenAsync("user2").get();
@@ -931,12 +984,21 @@ private String randomPhoneNumber() {
931984
}
932985

933986
private String signInWithCustomToken(String customToken) throws IOException {
934-
GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key="
987+
return signInWithCustomToken(customToken, null);
988+
}
989+
990+
private String signInWithCustomToken(
991+
String customToken, @Nullable String tenantId) throws IOException {
992+
final GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key="
935993
+ IntegrationTestUtils.getApiKey());
936-
Map<String, Object> content = ImmutableMap.<String, Object>of(
937-
"token", customToken, "returnSecureToken", true);
994+
ImmutableMap.Builder<String, Object> content = ImmutableMap.<String, Object>builder();
995+
content.put("token", customToken);
996+
content.put("returnSecureToken", true);
997+
if (tenantId != null) {
998+
content.put("tenantId", tenantId);
999+
}
9381000
HttpRequest request = transport.createRequestFactory().buildPostRequest(url,
939-
new JsonHttpContent(jsonFactory, content));
1001+
new JsonHttpContent(jsonFactory, content.build()));
9401002
request.setParser(new JsonObjectParser(jsonFactory));
9411003
HttpResponse response = request.execute();
9421004
try {

0 commit comments

Comments
 (0)