From ecdf709f2feec53a9a079b746d9e1e0e3b95b93d Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 5 Aug 2020 15:44:52 -0700 Subject: [PATCH 1/2] feat(auth): Support for creating tenant-scoped session cookies --- pom.xml | 4 +- .../firebase/auth/AbstractFirebaseAuth.java | 251 +++++++++++---- .../google/firebase/auth/FirebaseAuth.java | 209 +++--------- .../firebase/auth/FirebaseTokenUtils.java | 12 +- .../multitenancy/TenantAwareFirebaseAuth.java | 65 +++- .../auth/multitenancy/TenantManager.java | 2 +- .../firebase/auth/FirebaseAuthTest.java | 110 +++---- .../auth/FirebaseTokenVerifierImplTest.java | 13 + .../auth/FirebaseUserManagerTest.java | 25 +- .../firebase/auth/MockTokenVerifier.java | 59 ++++ .../TenantAwareFirebaseAuthTest.java | 297 ++++++++++++++++++ 11 files changed, 710 insertions(+), 337 deletions(-) create mode 100644 src/test/java/com/google/firebase/auth/MockTokenVerifier.java create mode 100644 src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java diff --git a/pom.xml b/pom.xml index adbf23931..b0187543a 100644 --- a/pom.xml +++ b/pom.xml @@ -198,7 +198,7 @@ org.jacoco jacoco-maven-plugin - 0.7.9 + 0.8.5 pre-unit-test @@ -289,7 +289,7 @@ maven-surefire-plugin - 2.19.1 + 2.22.0 ${skipUTs} diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index ad30d2cc3..6c841070c 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -62,50 +62,13 @@ public abstract class AbstractFirebaseAuth { private final Supplier userManager; private final JsonFactory jsonFactory; - protected AbstractFirebaseAuth(Builder builder) { + protected AbstractFirebaseAuth(Builder builder) { this.firebaseApp = checkNotNull(builder.firebaseApp); this.tokenFactory = threadSafeMemoize(builder.tokenFactory); this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier); this.cookieVerifier = threadSafeMemoize(builder.cookieVerifier); this.userManager = threadSafeMemoize(builder.userManager); - this.jsonFactory = builder.firebaseApp.getOptions().getJsonFactory(); - } - - protected static Builder builderFromAppAndTenantId(final FirebaseApp app, final String tenantId) { - return AbstractFirebaseAuth.builder() - .setFirebaseApp(app) - .setTokenFactory( - new Supplier() { - @Override - public FirebaseTokenFactory get() { - return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM, tenantId); - } - }) - .setIdTokenVerifier( - new Supplier() { - @Override - public FirebaseTokenVerifier get() { - return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM, tenantId); - } - }) - .setCookieVerifier( - new Supplier() { - @Override - public FirebaseTokenVerifier get() { - return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM); - } - }) - .setUserManager( - new Supplier() { - @Override - public FirebaseUserManager get() { - return FirebaseUserManager - .builder() - .setFirebaseApp(app) - .setTenantId(tenantId) - .build(); - } - }); + this.jsonFactory = firebaseApp.getOptions().getJsonFactory(); } /** @@ -220,6 +183,51 @@ public String execute() throws FirebaseAuthException { }; } + /** + * Creates a new Firebase session cookie from the given ID token and options. The returned JWT can + * be set as a server-side session cookie with a custom cookie policy. + * + * @param idToken The Firebase ID token to exchange for a session cookie. + * @param options Additional options required to create the cookie. + * @return A Firebase session cookie string. + * @throws IllegalArgumentException If the ID token is null or empty, or if options is null. + * @throws FirebaseAuthException If an error occurs while generating the session cookie. + */ + public String createSessionCookie(@NonNull String idToken, @NonNull SessionCookieOptions options) + throws FirebaseAuthException { + return createSessionCookieOp(idToken, options).call(); + } + + /** + * Similar to {@link #createSessionCookie(String, SessionCookieOptions)} but performs the + * operation asynchronously. + * + * @param idToken The Firebase ID token to exchange for a session cookie. + * @param options Additional options required to create the cookie. + * @return An {@code ApiFuture} which will complete successfully with a session cookie string. If + * an error occurs while generating the cookie or if the specified ID token is invalid, the + * future throws a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the ID token is null or empty, or if options is null. + */ + public ApiFuture createSessionCookieAsync( + @NonNull String idToken, @NonNull SessionCookieOptions options) { + return createSessionCookieOp(idToken, options).callAsync(firebaseApp); + } + + private CallableOperation createSessionCookieOp( + final String idToken, final SessionCookieOptions options) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(idToken), "idToken must not be null or empty"); + checkNotNull(options, "options must not be null"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected String execute() throws FirebaseAuthException { + return userManager.createSessionCookie(idToken, options); + } + }; + } + /** * Parses and verifies a Firebase ID Token. * @@ -320,6 +328,87 @@ FirebaseTokenVerifier getIdTokenVerifier(boolean checkRevoked) { return verifier; } + /** + * Parses and verifies a Firebase session cookie. + * + *

If verified successfully, returns a parsed version of the cookie from which the UID and the + * other claims can be read. If the cookie is invalid, throws a {@link FirebaseAuthException}. + * + *

This method does not check whether the cookie has been revoked. See {@link + * #verifySessionCookie(String, boolean)}. + * + * @param cookie A Firebase session cookie string to verify and parse. + * @return A {@link FirebaseToken} representing the verified and decoded cookie. + */ + public FirebaseToken verifySessionCookie(String cookie) throws FirebaseAuthException { + return verifySessionCookie(cookie, false); + } + + /** + * Parses and verifies a Firebase session cookie. + * + *

If {@code checkRevoked} is true, additionally verifies that the cookie has not been revoked. + * + *

If verified successfully, returns a parsed version of the cookie from which the UID and the + * other claims can be read. If the cookie is invalid or has been revoked while {@code + * checkRevoked} is true, throws a {@link FirebaseAuthException}. + * + * @param cookie A Firebase session cookie string to verify and parse. + * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked. + * @return A {@link FirebaseToken} representing the verified and decoded cookie. + */ + public FirebaseToken verifySessionCookie(String cookie, boolean checkRevoked) + throws FirebaseAuthException { + return verifySessionCookieOp(cookie, checkRevoked).call(); + } + + /** + * Similar to {@link #verifySessionCookie(String)} but performs the operation asynchronously. + * + * @param cookie A Firebase session cookie string to verify and parse. + * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or + * unsuccessfully with the failure Exception. + */ + public ApiFuture verifySessionCookieAsync(String cookie) { + return verifySessionCookieAsync(cookie, false); + } + + /** + * Similar to {@link #verifySessionCookie(String, boolean)} but performs the operation + * asynchronously. + * + * @param cookie A Firebase session cookie string to verify and parse. + * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked. + * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or + * unsuccessfully with the failure Exception. + */ + public ApiFuture verifySessionCookieAsync(String cookie, boolean checkRevoked) { + return verifySessionCookieOp(cookie, checkRevoked).callAsync(firebaseApp); + } + + private CallableOperation verifySessionCookieOp( + final String cookie, final boolean checkRevoked) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(cookie), "Session cookie must not be null or empty"); + final FirebaseTokenVerifier sessionCookieVerifier = getSessionCookieVerifier(checkRevoked); + return new CallableOperation() { + @Override + public FirebaseToken execute() throws FirebaseAuthException { + return sessionCookieVerifier.verifyToken(cookie); + } + }; + } + + @VisibleForTesting + FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) { + FirebaseTokenVerifier verifier = cookieVerifier.get(); + if (checkRevoked) { + FirebaseUserManager userManager = getUserManager(); + verifier = RevocationCheckDecorator.decorateSessionCookieVerifier(verifier, userManager); + } + return verifier; + } + /** * Revokes all refresh tokens for the specified user. * @@ -1637,19 +1726,11 @@ protected Void execute() throws FirebaseAuthException { }; } - FirebaseApp getFirebaseApp() { - return this.firebaseApp; - } - - FirebaseTokenVerifier getCookieVerifier() { - return this.cookieVerifier.get(); - } - FirebaseUserManager getUserManager() { return this.userManager.get(); } - protected Supplier threadSafeMemoize(final Supplier supplier) { + Supplier threadSafeMemoize(final Supplier supplier) { return Suppliers.memoize( new Supplier() { @Override @@ -1663,7 +1744,7 @@ public T get() { }); } - void checkNotDestroyed() { + private void checkNotDestroyed() { synchronized (lock) { checkState( !destroyed.get(), @@ -1682,42 +1763,80 @@ final void destroy() { /** Performs any additional required clean up. */ protected abstract void doDestroy(); - static Builder builder() { - return new Builder(); - } + protected abstract static class Builder> { - static class Builder { - protected FirebaseApp firebaseApp; + private FirebaseApp firebaseApp; private Supplier tokenFactory; private Supplier idTokenVerifier; private Supplier cookieVerifier; - private Supplier userManager; + private Supplier userManager; - private Builder() {} + protected abstract T getThis(); + + public FirebaseApp getFirebaseApp() { + return firebaseApp; + } - Builder setFirebaseApp(FirebaseApp firebaseApp) { + public T setFirebaseApp(FirebaseApp firebaseApp) { this.firebaseApp = firebaseApp; - return this; + return getThis(); } - Builder setTokenFactory(Supplier tokenFactory) { + public T setTokenFactory(Supplier tokenFactory) { this.tokenFactory = tokenFactory; - return this; + return getThis(); } - Builder setIdTokenVerifier(Supplier idTokenVerifier) { + public T setIdTokenVerifier(Supplier idTokenVerifier) { this.idTokenVerifier = idTokenVerifier; - return this; + return getThis(); } - Builder setCookieVerifier(Supplier cookieVerifier) { + public T setCookieVerifier(Supplier cookieVerifier) { this.cookieVerifier = cookieVerifier; - return this; + return getThis(); } - Builder setUserManager(Supplier userManager) { + public T setUserManager(Supplier userManager) { this.userManager = userManager; - return this; + return getThis(); } } + + protected static > T populateBuilderFromApp( + Builder builder, final FirebaseApp app, @Nullable final String tenantId) { + return builder.setFirebaseApp(app) + .setTokenFactory( + new Supplier() { + @Override + public FirebaseTokenFactory get() { + return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM, tenantId); + } + }) + .setIdTokenVerifier( + new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM, tenantId); + } + }) + .setCookieVerifier( + new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM, tenantId); + } + }) + .setUserManager( + new Supplier() { + @Override + public FirebaseUserManager get() { + return FirebaseUserManager + .builder() + .setFirebaseApp(app) + .setTenantId(tenantId) + .build(); + } + }); + } } diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index f44bd2343..417ea6436 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -16,21 +16,11 @@ package com.google.firebase.auth; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.api.client.util.Clock; -import com.google.api.core.ApiFuture; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; -import com.google.firebase.auth.internal.FirebaseTokenFactory; import com.google.firebase.auth.multitenancy.TenantManager; -import com.google.firebase.internal.CallableOperation; import com.google.firebase.internal.FirebaseService; -import com.google.firebase.internal.NonNull; /** * This class is the entry point for all server-side Firebase Authentication actions. @@ -46,14 +36,9 @@ public final class FirebaseAuth extends AbstractFirebaseAuth { private final Supplier tenantManager; - FirebaseAuth(final Builder builder) { + private FirebaseAuth(final Builder builder) { super(builder); - tenantManager = threadSafeMemoize(new Supplier() { - @Override - public TenantManager get() { - return new TenantManager(builder.firebaseApp); - } - }); + tenantManager = threadSafeMemoize(builder.tenantManager); } public TenantManager getTenantManager() { @@ -84,167 +69,18 @@ public static synchronized FirebaseAuth getInstance(FirebaseApp app) { return service.getInstance(); } - /** - * Creates a new Firebase session cookie from the given ID token and options. The returned JWT can - * be set as a server-side session cookie with a custom cookie policy. - * - * @param idToken The Firebase ID token to exchange for a session cookie. - * @param options Additional options required to create the cookie. - * @return A Firebase session cookie string. - * @throws IllegalArgumentException If the ID token is null or empty, or if options is null. - * @throws FirebaseAuthException If an error occurs while generating the session cookie. - */ - public String createSessionCookie(@NonNull String idToken, @NonNull SessionCookieOptions options) - throws FirebaseAuthException { - return createSessionCookieOp(idToken, options).call(); - } - - /** - * Similar to {@link #createSessionCookie(String, SessionCookieOptions)} but performs the - * operation asynchronously. - * - * @param idToken The Firebase ID token to exchange for a session cookie. - * @param options Additional options required to create the cookie. - * @return An {@code ApiFuture} which will complete successfully with a session cookie string. If - * an error occurs while generating the cookie or if the specified ID token is invalid, the - * future throws a {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the ID token is null or empty, or if options is null. - */ - public ApiFuture createSessionCookieAsync( - @NonNull String idToken, @NonNull SessionCookieOptions options) { - return createSessionCookieOp(idToken, options).callAsync(getFirebaseApp()); - } - - private CallableOperation createSessionCookieOp( - final String idToken, final SessionCookieOptions options) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(idToken), "idToken must not be null or empty"); - checkNotNull(options, "options must not be null"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected String execute() throws FirebaseAuthException { - return userManager.createSessionCookie(idToken, options); - } - }; - } - - /** - * Parses and verifies a Firebase session cookie. - * - *

If verified successfully, returns a parsed version of the cookie from which the UID and the - * other claims can be read. If the cookie is invalid, throws a {@link FirebaseAuthException}. - * - *

This method does not check whether the cookie has been revoked. See {@link - * #verifySessionCookie(String, boolean)}. - * - * @param cookie A Firebase session cookie string to verify and parse. - * @return A {@link FirebaseToken} representing the verified and decoded cookie. - */ - public FirebaseToken verifySessionCookie(String cookie) throws FirebaseAuthException { - return verifySessionCookie(cookie, false); - } - - /** - * Parses and verifies a Firebase session cookie. - * - *

If {@code checkRevoked} is true, additionally verifies that the cookie has not been revoked. - * - *

If verified successfully, returns a parsed version of the cookie from which the UID and the - * other claims can be read. If the cookie is invalid or has been revoked while {@code - * checkRevoked} is true, throws a {@link FirebaseAuthException}. - * - * @param cookie A Firebase session cookie string to verify and parse. - * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked. - * @return A {@link FirebaseToken} representing the verified and decoded cookie. - */ - public FirebaseToken verifySessionCookie(String cookie, boolean checkRevoked) - throws FirebaseAuthException { - return verifySessionCookieOp(cookie, checkRevoked).call(); - } - - /** - * Similar to {@link #verifySessionCookie(String)} but performs the operation asynchronously. - * - * @param cookie A Firebase session cookie string to verify and parse. - * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or - * unsuccessfully with the failure Exception. - */ - public ApiFuture verifySessionCookieAsync(String cookie) { - return verifySessionCookieAsync(cookie, false); - } - - /** - * Similar to {@link #verifySessionCookie(String, boolean)} but performs the operation - * asynchronously. - * - * @param cookie A Firebase session cookie string to verify and parse. - * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked. - * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or - * unsuccessfully with the failure Exception. - */ - public ApiFuture verifySessionCookieAsync(String cookie, boolean checkRevoked) { - return verifySessionCookieOp(cookie, checkRevoked).callAsync(getFirebaseApp()); - } - - private CallableOperation verifySessionCookieOp( - final String cookie, final boolean checkRevoked) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(cookie), "Session cookie must not be null or empty"); - final FirebaseTokenVerifier sessionCookieVerifier = getSessionCookieVerifier(checkRevoked); - return new CallableOperation() { - @Override - public FirebaseToken execute() throws FirebaseAuthException { - return sessionCookieVerifier.verifyToken(cookie); - } - }; - } - - @VisibleForTesting - FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) { - FirebaseTokenVerifier verifier = getCookieVerifier(); - if (checkRevoked) { - FirebaseUserManager userManager = getUserManager(); - verifier = RevocationCheckDecorator.decorateSessionCookieVerifier(verifier, userManager); - } - return verifier; - } - @Override protected void doDestroy() { } private static FirebaseAuth fromApp(final FirebaseApp app) { - return new FirebaseAuth( - AbstractFirebaseAuth.builder() - .setFirebaseApp(app) - .setTokenFactory( - new Supplier() { - @Override - public FirebaseTokenFactory get() { - return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM); - } - }) - .setIdTokenVerifier( - new Supplier() { - @Override - public FirebaseTokenVerifier get() { - return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM); - } - }) - .setCookieVerifier( - new Supplier() { - @Override - public FirebaseTokenVerifier get() { - return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM); - } - }) - .setUserManager( - new Supplier() { - @Override - public FirebaseUserManager get() { - return FirebaseUserManager.builder().setFirebaseApp(app).build(); - } - })); + return populateBuilderFromApp(builder(), app, null) + .setTenantManager(new Supplier() { + @Override + public TenantManager get() { + return new TenantManager(app); + } + }) + .build(); } private static class FirebaseAuthService extends FirebaseService { @@ -258,4 +94,29 @@ public void destroy() { instance.destroy(); } } + + static Builder builder() { + return new Builder(); + } + + static class Builder extends AbstractFirebaseAuth.Builder { + + private Supplier tenantManager; + + private Builder() { } + + @Override + protected Builder getThis() { + return this; + } + + public Builder setTenantManager(Supplier tenantManager) { + this.tenantManager = tenantManager; + return this; + } + + public FirebaseAuth build() { + return new FirebaseAuth(this); + } + } } diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java index e0105e9aa..7c8f9f0a5 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java @@ -99,6 +99,11 @@ static FirebaseTokenVerifierImpl createIdTokenVerifier( } static FirebaseTokenVerifierImpl createSessionCookieVerifier(FirebaseApp app, Clock clock) { + return createSessionCookieVerifier(app, clock, null); + } + + static FirebaseTokenVerifierImpl createSessionCookieVerifier( + 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 verifySessionCookie()"); @@ -107,12 +112,13 @@ static FirebaseTokenVerifierImpl createSessionCookieVerifier(FirebaseApp app, Cl GooglePublicKeysManager publicKeysManager = newPublicKeysManager( app.getOptions(), clock, SESSION_COOKIE_CERT_URL); return FirebaseTokenVerifierImpl.builder() - .setJsonFactory(app.getOptions().getJsonFactory()) - .setPublicKeysManager(publicKeysManager) - .setIdTokenVerifier(idTokenVerifier) .setShortName("session cookie") .setMethod("verifySessionCookie()") .setDocUrl("https://firebase.google.com/docs/auth/admin/manage-cookies") + .setJsonFactory(app.getOptions().getJsonFactory()) + .setPublicKeysManager(publicKeysManager) + .setIdTokenVerifier(idTokenVerifier) + .setTenantId(tenantId) .build(); } diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java index 540437404..95ef169da 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java @@ -18,9 +18,16 @@ import static com.google.common.base.Preconditions.checkArgument; +import com.google.api.core.ApiAsyncFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; import com.google.common.base.Strings; +import com.google.common.util.concurrent.MoreExecutors; import com.google.firebase.FirebaseApp; import com.google.firebase.auth.AbstractFirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.FirebaseToken; +import com.google.firebase.auth.SessionCookieOptions; /** * The tenant-aware Firebase client. @@ -32,10 +39,10 @@ public final class TenantAwareFirebaseAuth extends AbstractFirebaseAuth { private final String tenantId; - TenantAwareFirebaseAuth(final FirebaseApp firebaseApp, final String tenantId) { - super(builderFromAppAndTenantId(firebaseApp, tenantId)); - checkArgument(!Strings.isNullOrEmpty(tenantId)); - this.tenantId = tenantId; + private TenantAwareFirebaseAuth(Builder builder) { + super(builder); + checkArgument(!Strings.isNullOrEmpty(builder.tenantId)); + this.tenantId = builder.tenantId; } /** Returns the client's tenant ID. */ @@ -43,8 +50,58 @@ public String getTenantId() { return tenantId; } + @Override + public String createSessionCookie( + String idToken, SessionCookieOptions options) throws FirebaseAuthException { + verifyIdToken(idToken); + return super.createSessionCookie(idToken, options); + } + + @Override + public ApiFuture createSessionCookieAsync( + final String idToken, final SessionCookieOptions options) { + ApiFuture future = verifyIdTokenAsync(idToken); + return ApiFutures.transformAsync(future, new ApiAsyncFunction() { + @Override + public ApiFuture apply(FirebaseToken input) { + return TenantAwareFirebaseAuth.super.createSessionCookieAsync(idToken, options); + } + }, MoreExecutors.directExecutor()); + } + @Override protected void doDestroy() { // Nothing extra needs to be destroyed. } + + static TenantAwareFirebaseAuth fromApp(FirebaseApp app, String tenantId) { + return populateBuilderFromApp(builder(), app, tenantId) + .setTenantId(tenantId) + .build(); + } + + static Builder builder() { + return new Builder(); + } + + static class Builder extends AbstractFirebaseAuth.Builder { + + private String tenantId; + + private Builder() { } + + @Override + protected Builder getThis() { + return this; + } + + public Builder setTenantId(String tenantId) { + this.tenantId = tenantId; + return this; + } + + TenantAwareFirebaseAuth build() { + return new TenantAwareFirebaseAuth(this); + } + } } diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java index 11f26b096..dcb226d28 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java @@ -79,7 +79,7 @@ public Tenant getTenant(@NonNull String tenantId) throws FirebaseAuthException { public synchronized TenantAwareFirebaseAuth getAuthForTenant(@NonNull String tenantId) { checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); if (!tenantAwareAuths.containsKey(tenantId)) { - tenantAwareAuths.put(tenantId, new TenantAwareFirebaseAuth(firebaseApp, tenantId)); + tenantAwareAuths.put(tenantId, TenantAwareFirebaseAuth.fromApp(firebaseApp, tenantId)); } return tenantAwareAuths.get(tenantId); } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index 635e1bfba..bfc8d1790 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertEquals; 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.assertTrue; import static org.junit.Assert.fail; @@ -28,11 +29,11 @@ import com.google.common.base.Defaults; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; -import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.testing.ServiceAccount; +import com.google.firebase.testing.TestResponseInterceptor; import com.google.firebase.testing.TestUtils; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -43,7 +44,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; - import org.junit.After; import org.junit.Test; @@ -169,8 +169,7 @@ public void testDefaultIdTokenVerifier() { @Test public void testIdTokenVerifierInitializedOnDemand() throws Exception { - FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( - getFirebaseToken("idTokenUser")); + FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("idTokenUser"); CountingSupplier countingSupplier = new CountingSupplier<>( Suppliers.ofInstance(tokenVerifier)); @@ -185,37 +184,33 @@ public void testIdTokenVerifierInitializedOnDemand() throws Exception { @Test public void testVerifyIdTokenWithNull() { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null); - tokenVerifier.lastTokenString = "_init_"; + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); try { auth.verifyIdTokenAsync(null); fail("No error thrown for null id token"); } catch (IllegalArgumentException expected) { - assertEquals("_init_", tokenVerifier.getLastTokenString()); + assertNull(tokenVerifier.getLastTokenString()); } } @Test public void testVerifyIdTokenWithEmptyString() { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null); - tokenVerifier.lastTokenString = "_init_"; + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); try { auth.verifyIdTokenAsync(""); fail("No error thrown for null id token"); } catch (IllegalArgumentException expected) { - assertEquals("_init_", tokenVerifier.getLastTokenString()); + assertNull(tokenVerifier.getLastTokenString()); } - } @Test public void testVerifyIdToken() throws Exception { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( - getFirebaseToken("testUser")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("testUser"); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); FirebaseToken firebaseToken = auth.verifyIdToken("idtoken"); @@ -242,8 +237,7 @@ public void testVerifyIdTokenFailure() { @Test public void testVerifyIdTokenAsync() throws Exception { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( - getFirebaseToken("testUser")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("testUser"); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); FirebaseToken firebaseToken = auth.verifyIdTokenAsync("idtoken").get(); @@ -300,8 +294,7 @@ public void testDefaultSessionCookieVerifier() { @Test public void testSessionCookieVerifierInitializedOnDemand() throws Exception { - FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( - getFirebaseToken("cookieUser")); + FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("cookieUser"); CountingSupplier countingSupplier = new CountingSupplier<>( Suppliers.ofInstance(tokenVerifier)); FirebaseAuth auth = getAuthForSessionCookieVerification(countingSupplier); @@ -316,37 +309,33 @@ public void testSessionCookieVerifierInitializedOnDemand() throws Exception { @Test public void testVerifySessionCookieWithNull() { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null); - tokenVerifier.lastTokenString = "_init_"; + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); try { auth.verifySessionCookieAsync(null); fail("No error thrown for null id token"); } catch (IllegalArgumentException expected) { - assertEquals("_init_", tokenVerifier.getLastTokenString()); + assertNull(tokenVerifier.getLastTokenString()); } } @Test public void testVerifySessionCookieWithEmptyString() { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null); - tokenVerifier.lastTokenString = "_init_"; + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); try { auth.verifySessionCookieAsync(""); fail("No error thrown for null id token"); } catch (IllegalArgumentException expected) { - assertEquals("_init_", tokenVerifier.getLastTokenString()); + assertNull(tokenVerifier.getLastTokenString()); } - } @Test public void testVerifySessionCookie() throws Exception { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( - getFirebaseToken("testUser")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("testUser"); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); FirebaseToken firebaseToken = auth.verifySessionCookie("idtoken"); @@ -373,8 +362,7 @@ public void testVerifySessionCookieFailure() { @Test public void testVerifySessionCookieAsync() throws Exception { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( - getFirebaseToken("testUser")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("testUser"); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); FirebaseToken firebaseToken = auth.verifySessionCookieAsync("idtoken").get(); @@ -417,10 +405,6 @@ public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws Interru } } - private FirebaseToken getFirebaseToken(String subject) { - return new FirebaseToken(ImmutableMap.of("sub", subject)); - } - private FirebaseAuth getAuthForIdTokenVerification(FirebaseTokenVerifier tokenVerifier) { return getAuthForIdTokenVerification(Suppliers.ofInstance(tokenVerifier)); } @@ -429,11 +413,11 @@ private FirebaseAuth getAuthForIdTokenVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); FirebaseUserManager userManager = FirebaseUserManager.builder().setFirebaseApp(app).build(); - return new FirebaseAuth( - AbstractFirebaseAuth.builder() - .setFirebaseApp(app) - .setIdTokenVerifier(tokenVerifierSupplier) - .setUserManager(Suppliers.ofInstance(userManager))); + return FirebaseAuth.builder() + .setFirebaseApp(app) + .setIdTokenVerifier(tokenVerifierSupplier) + .setUserManager(Suppliers.ofInstance(userManager)) + .build(); } private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier tokenVerifier) { @@ -444,45 +428,23 @@ private FirebaseAuth getAuthForSessionCookieVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); FirebaseUserManager userManager = FirebaseUserManager.builder().setFirebaseApp(app).build(); - return new FirebaseAuth( - AbstractFirebaseAuth.builder() - .setFirebaseApp(app) - .setCookieVerifier(tokenVerifierSupplier) - .setUserManager(Suppliers.ofInstance(userManager))); + return FirebaseAuth.builder() + .setFirebaseApp(app) + .setCookieVerifier(tokenVerifierSupplier) + .setUserManager(Suppliers.ofInstance(userManager)) + .build(); } - private static class MockTokenVerifier implements FirebaseTokenVerifier { - - private String lastTokenString; - - private FirebaseToken result; - private FirebaseAuthException exception; - - private MockTokenVerifier(FirebaseToken result, FirebaseAuthException exception) { - this.result = result; - this.exception = exception; - } - - @Override - public FirebaseToken verifyToken(String token) throws FirebaseAuthException { - lastTokenString = token; - if (exception != null) { - throw exception; - } - return result; - } - - String getLastTokenString() { - return this.lastTokenString; - } - - static MockTokenVerifier fromResult(FirebaseToken result) { - return new MockTokenVerifier(result, null); - } - - static MockTokenVerifier fromException(FirebaseAuthException exception) { - return new MockTokenVerifier(null, exception); - } + public static TestResponseInterceptor setUserManager( + AbstractFirebaseAuth.Builder builder, FirebaseApp app, String tenantId) { + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseUserManager userManager = FirebaseUserManager.builder() + .setFirebaseApp(app) + .setTenantId(tenantId) + .build(); + userManager.setInterceptor(interceptor); + builder.setUserManager(Suppliers.ofInstance(userManager)); + return interceptor; } private static class CountingSupplier implements Supplier { diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java index 5dd1e9c14..379da0a57 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java @@ -217,6 +217,19 @@ public void testMalformedToken() throws Exception { tokenVerifier.verifyToken("not.a.jwt"); } + @Test + public void testVerifyTokenWithTenantId() throws FirebaseAuthException { + FirebaseTokenVerifierImpl verifier = fullyPopulatedBuilder() + .setTenantId("TENANT_1") + .build(); + + FirebaseToken firebaseToken = verifier.verifyToken(createTokenWithTenantId("TENANT_1")); + + assertEquals(TEST_TOKEN_ISSUER, firebaseToken.getIssuer()); + assertEquals(TestTokenFactory.UID, firebaseToken.getUid()); + assertEquals("TENANT_1", firebaseToken.getTenantId()); + } + @Test public void testVerifyTokenDifferentTenantIds() { try { diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index d407fd8ec..7012ef6bf 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -2602,19 +2602,18 @@ private static FirebaseAuth getRetryDisabledAuth(MockLowLevelHttpResponse respon .setProjectId("test-project-id") .setHttpTransport(transport) .build()); - return new FirebaseAuth( - AbstractFirebaseAuth.builder() - .setFirebaseApp(app) - .setUserManager(new Supplier() { - @Override - public FirebaseUserManager get() { - return FirebaseUserManager - .builder() - .setFirebaseApp(app) - .setHttpRequestFactory(transport.createRequestFactory()) - .build(); - } - })); + return FirebaseAuth.builder() + .setFirebaseApp(app) + .setUserManager(new Supplier() { + @Override + public FirebaseUserManager get() { + return FirebaseUserManager.builder() + .setFirebaseApp(app) + .setHttpRequestFactory(transport.createRequestFactory()) + .build(); + } + }) + .build(); } private static void checkUserRecord(UserRecord userRecord) { diff --git a/src/test/java/com/google/firebase/auth/MockTokenVerifier.java b/src/test/java/com/google/firebase/auth/MockTokenVerifier.java new file mode 100644 index 000000000..51f7d5a47 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/MockTokenVerifier.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020 Google LLC + * + * 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; + +import com.google.common.collect.ImmutableMap; +import java.util.concurrent.atomic.AtomicReference; + +public class MockTokenVerifier implements FirebaseTokenVerifier { + + private final FirebaseToken result; + private final FirebaseAuthException exception; + private final AtomicReference lastTokenString = new AtomicReference<>(); + + private MockTokenVerifier(FirebaseToken result, FirebaseAuthException exception) { + this.result = result; + this.exception = exception; + } + + @Override + public FirebaseToken verifyToken(String token) throws FirebaseAuthException { + lastTokenString.set(token); + if (exception != null) { + throw exception; + } + return result; + } + + public String getLastTokenString() { + return this.lastTokenString.get(); + } + + public static MockTokenVerifier fromUid(String uid) { + long iat = System.currentTimeMillis() / 1000; + return fromResult(new FirebaseToken(ImmutableMap.of( + "sub", uid, "iat", iat))); + } + + public static MockTokenVerifier fromResult(FirebaseToken result) { + return new MockTokenVerifier(result, null); + } + + public static MockTokenVerifier fromException(FirebaseAuthException exception) { + return new MockTokenVerifier(null, exception); + } +} diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java new file mode 100644 index 000000000..7fe85705d --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java @@ -0,0 +1,297 @@ +/* + * Copyright 2020 Google LLC + * + * 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.multitenancy; + +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.api.client.googleapis.util.Utils; +import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.json.GenericJson; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.common.base.Suppliers; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.FirebaseAuthTest; +import com.google.firebase.auth.FirebaseToken; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.auth.MockTokenVerifier; +import com.google.firebase.auth.SessionCookieOptions; +import com.google.firebase.testing.MultiRequestMockHttpTransport; +import com.google.firebase.testing.TestResponseInterceptor; +import com.google.firebase.testing.TestUtils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Test; + +public class TenantAwareFirebaseAuthTest { + + private static final String AUTH_BASE_URL = "/v1/projects/test-project-id/tenants/test-tenant"; + + private static final SessionCookieOptions COOKIE_OPTIONS = SessionCookieOptions.builder() + .setExpiresIn(TimeUnit.HOURS.toMillis(1)) + .build(); + + private static final FirebaseAuthException AUTH_EXCEPTION = new FirebaseAuthException( + "code", "reason"); + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testCreateSessionCookieAsync() throws Exception { + MockTokenVerifier verifier = MockTokenVerifier.fromUid("uid"); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(verifier); + TestResponseInterceptor interceptor = setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + String cookie = auth.createSessionCookieAsync("testToken", COOKIE_OPTIONS).get(); + + assertEquals("MockCookieString", cookie); + assertEquals("testToken", verifier.getLastTokenString()); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals(2, parsed.size()); + assertEquals("testToken", parsed.get("idToken")); + assertEquals(new BigDecimal(3600), parsed.get("validDuration")); + checkUrl(interceptor, AUTH_BASE_URL + ":createSessionCookie"); + } + + @Test + public void testCreateSessionCookieAsyncError() throws Exception { + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification( + MockTokenVerifier.fromException(AUTH_EXCEPTION)); + TestResponseInterceptor interceptor = setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + try { + auth.createSessionCookieAsync("testToken", COOKIE_OPTIONS).get(); + fail("No error thrown for invalid ID token"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + FirebaseAuthException cause = (FirebaseAuthException) e.getCause(); + assertEquals("code", cause.getErrorCode()); + } + + assertNull(interceptor.getResponse()); + } + + @Test + public void testCreateSessionCookie() throws Exception { + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification( + MockTokenVerifier.fromUid("uid")); + TestResponseInterceptor interceptor = setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + String cookie = auth.createSessionCookie("testToken", COOKIE_OPTIONS); + + assertEquals("MockCookieString", cookie); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals(2, parsed.size()); + assertEquals("testToken", parsed.get("idToken")); + assertEquals(new BigDecimal(3600), parsed.get("validDuration")); + checkUrl(interceptor, AUTH_BASE_URL + ":createSessionCookie"); + } + + @Test + public void testCreateSessionCookieError() { + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification( + MockTokenVerifier.fromException(AUTH_EXCEPTION)); + TestResponseInterceptor interceptor = setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + try { + auth.createSessionCookie("testToken", COOKIE_OPTIONS); + fail("No error thrown for invalid ID token"); + } catch (FirebaseAuthException e) { + assertEquals("code", e.getErrorCode()); + } + + assertNull(interceptor.getResponse()); + } + + @Test + public void testVerifySessionCookie() throws FirebaseAuthException { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + FirebaseToken token = auth.verifySessionCookie("cookie"); + + assertEquals("uid", token.getUid()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + + @Test + public void testVerifySessionCookieFailure() { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(AUTH_EXCEPTION); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + initUserManagerForGetUser(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + try { + auth.verifySessionCookie("cookie"); + fail("No error thrown for invalid token"); + } catch (FirebaseAuthException authException) { + assertEquals("code", authException.getErrorCode()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + } + + @Test + public void testVerifySessionCookieAsync() throws Exception { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + FirebaseToken firebaseToken = auth.verifySessionCookieAsync("cookie").get(); + + assertEquals("uid", firebaseToken.getUid()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + + @Test + public void testVerifySessionCookieAsyncFailure() throws InterruptedException { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(AUTH_EXCEPTION); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + try { + auth.verifySessionCookieAsync("cookie").get(); + fail("No error thrown for invalid token"); + } catch (ExecutionException e) { + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals("code", authException.getErrorCode()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + } + + @Test + public void testVerifySessionCookieWithCheckRevoked() throws FirebaseAuthException { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + TestResponseInterceptor interceptor = initUserManagerForGetUser(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + FirebaseToken token = auth.verifySessionCookie("cookie", true); + + assertEquals("uid", token.getUid()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + checkUrl(interceptor, AUTH_BASE_URL + "/accounts:lookup"); + } + + @Test + public void testVerifySessionCookieWithCheckRevokedFailure() { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(AUTH_EXCEPTION); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + try { + auth.verifySessionCookie("cookie", true); + fail("No error thrown for invalid token"); + } catch (FirebaseAuthException e) { + assertEquals("code", e.getErrorCode()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + } + + @Test + public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws InterruptedException { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(AUTH_EXCEPTION); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + try { + auth.verifySessionCookieAsync("cookie", true).get(); + fail("No error thrown for invalid token"); + } catch (ExecutionException e) { + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals("code", authException.getErrorCode()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + } + + private static TenantAwareFirebaseAuth.Builder builderForTokenVerification( + MockTokenVerifier verifier) { + return TenantAwareFirebaseAuth.builder() + .setIdTokenVerifier(Suppliers.ofInstance(verifier)) + .setCookieVerifier(Suppliers.ofInstance(verifier)); + } + + private static TestResponseInterceptor setUserManager(TenantAwareFirebaseAuth.Builder builder) { + FirebaseApp app = initializeAppWithResponses( + TestUtils.loadResource("createSessionCookie.json")); + builder.setFirebaseApp(app); + builder.setTenantId("test-tenant"); + return FirebaseAuthTest.setUserManager(builder, app, "test-tenant"); + } + + private static TestResponseInterceptor initUserManagerForGetUser( + TenantAwareFirebaseAuth.Builder builder) { + FirebaseApp app = initializeAppWithResponses( + TestUtils.loadResource("getUser.json")); + builder.setFirebaseApp(app); + builder.setTenantId("test-tenant"); + return FirebaseAuthTest.setUserManager(builder, app, "test-tenant"); + } + + private static FirebaseApp initializeAppWithResponses(String... responses) { + List mocks = new ArrayList<>(); + for (String response : responses) { + mocks.add(new MockLowLevelHttpResponse().setContent(response)); + } + + MockHttpTransport transport = new MultiRequestMockHttpTransport(mocks); + return FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("token")) + .setHttpTransport(transport) + .setProjectId("test-project-id") + .build()); + } + + private static GenericJson parseRequestContent(TestResponseInterceptor interceptor) + throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + interceptor.getResponse().getRequest().getContent().writeTo(out); + return Utils.getDefaultJsonFactory().fromString( + new String(out.toByteArray()), GenericJson.class); + } + + private static void checkUrl(TestResponseInterceptor interceptor, String url) { + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals(HttpMethods.POST, request.getRequestMethod()); + assertEquals(url, request.getUrl().getRawPath()); + } +} From ced48123740fc665e5cca402180bd64674841026 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 5 Aug 2020 16:02:26 -0700 Subject: [PATCH 2/2] fix: Cleaned up the unit test --- .../TenantAwareFirebaseAuthTest.java | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java index 7fe85705d..df7bcf00d 100644 --- a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java @@ -37,14 +37,11 @@ import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.auth.MockTokenVerifier; import com.google.firebase.auth.SessionCookieOptions; -import com.google.firebase.testing.MultiRequestMockHttpTransport; import com.google.firebase.testing.TestResponseInterceptor; import com.google.firebase.testing.TestUtils; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.junit.After; @@ -52,7 +49,9 @@ public class TenantAwareFirebaseAuthTest { - private static final String AUTH_BASE_URL = "/v1/projects/test-project-id/tenants/test-tenant"; + private static final String TENANT_ID = "test-tenant"; + + private static final String AUTH_BASE_URL = "/v1/projects/test-project-id/tenants/" + TENANT_ID; private static final SessionCookieOptions COOKIE_OPTIONS = SessionCookieOptions.builder() .setExpiresIn(TimeUnit.HOURS.toMillis(1)) @@ -61,6 +60,9 @@ public class TenantAwareFirebaseAuthTest { private static final FirebaseAuthException AUTH_EXCEPTION = new FirebaseAuthException( "code", "reason"); + private static final String CREATE_COOKIE_RESPONSE = TestUtils.loadResource( + "createSessionCookie.json"); + @After public void tearDown() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -70,7 +72,7 @@ public void tearDown() { public void testCreateSessionCookieAsync() throws Exception { MockTokenVerifier verifier = MockTokenVerifier.fromUid("uid"); TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(verifier); - TestResponseInterceptor interceptor = setUserManager(builder); + TestResponseInterceptor interceptor = setUserManager(builder, CREATE_COOKIE_RESPONSE); TenantAwareFirebaseAuth auth = builder.build(); String cookie = auth.createSessionCookieAsync("testToken", COOKIE_OPTIONS).get(); @@ -88,7 +90,7 @@ public void testCreateSessionCookieAsync() throws Exception { public void testCreateSessionCookieAsyncError() throws Exception { TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification( MockTokenVerifier.fromException(AUTH_EXCEPTION)); - TestResponseInterceptor interceptor = setUserManager(builder); + TestResponseInterceptor interceptor = setUserManager(builder, CREATE_COOKIE_RESPONSE); TenantAwareFirebaseAuth auth = builder.build(); try { @@ -107,7 +109,7 @@ public void testCreateSessionCookieAsyncError() throws Exception { public void testCreateSessionCookie() throws Exception { TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification( MockTokenVerifier.fromUid("uid")); - TestResponseInterceptor interceptor = setUserManager(builder); + TestResponseInterceptor interceptor = setUserManager(builder, CREATE_COOKIE_RESPONSE); TenantAwareFirebaseAuth auth = builder.build(); String cookie = auth.createSessionCookie("testToken", COOKIE_OPTIONS); @@ -124,7 +126,7 @@ public void testCreateSessionCookie() throws Exception { public void testCreateSessionCookieError() { TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification( MockTokenVerifier.fromException(AUTH_EXCEPTION)); - TestResponseInterceptor interceptor = setUserManager(builder); + TestResponseInterceptor interceptor = setUserManager(builder, CREATE_COOKIE_RESPONSE); TenantAwareFirebaseAuth auth = builder.build(); try { @@ -154,7 +156,7 @@ public void testVerifySessionCookie() throws FirebaseAuthException { public void testVerifySessionCookieFailure() { MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(AUTH_EXCEPTION); TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); - initUserManagerForGetUser(builder); + setUserManager(builder); TenantAwareFirebaseAuth auth = builder.build(); try { @@ -200,7 +202,8 @@ public void testVerifySessionCookieAsyncFailure() throws InterruptedException { public void testVerifySessionCookieWithCheckRevoked() throws FirebaseAuthException { MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); - TestResponseInterceptor interceptor = initUserManagerForGetUser(builder); + TestResponseInterceptor interceptor = setUserManager( + builder, TestUtils.loadResource("getUser.json")); TenantAwareFirebaseAuth auth = builder.build(); FirebaseToken token = auth.verifySessionCookie("cookie", true); @@ -246,34 +249,26 @@ public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws Interru private static TenantAwareFirebaseAuth.Builder builderForTokenVerification( MockTokenVerifier verifier) { return TenantAwareFirebaseAuth.builder() + .setTenantId(TENANT_ID) .setIdTokenVerifier(Suppliers.ofInstance(verifier)) .setCookieVerifier(Suppliers.ofInstance(verifier)); } private static TestResponseInterceptor setUserManager(TenantAwareFirebaseAuth.Builder builder) { - FirebaseApp app = initializeAppWithResponses( - TestUtils.loadResource("createSessionCookie.json")); - builder.setFirebaseApp(app); - builder.setTenantId("test-tenant"); - return FirebaseAuthTest.setUserManager(builder, app, "test-tenant"); + return setUserManager(builder, "{}"); } - private static TestResponseInterceptor initUserManagerForGetUser( - TenantAwareFirebaseAuth.Builder builder) { - FirebaseApp app = initializeAppWithResponses( - TestUtils.loadResource("getUser.json")); + private static TestResponseInterceptor setUserManager( + TenantAwareFirebaseAuth.Builder builder, String response) { + FirebaseApp app = initializeAppWithResponse(response); builder.setFirebaseApp(app); - builder.setTenantId("test-tenant"); - return FirebaseAuthTest.setUserManager(builder, app, "test-tenant"); + return FirebaseAuthTest.setUserManager(builder, app, TENANT_ID); } - private static FirebaseApp initializeAppWithResponses(String... responses) { - List mocks = new ArrayList<>(); - for (String response : responses) { - mocks.add(new MockLowLevelHttpResponse().setContent(response)); - } - - MockHttpTransport transport = new MultiRequestMockHttpTransport(mocks); + private static FirebaseApp initializeAppWithResponse(String response) { + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent(response)) + .build(); return FirebaseApp.initializeApp(new FirebaseOptions.Builder() .setCredentials(new MockGoogleCredentials("token")) .setHttpTransport(transport)