Skip to content

Commit 3564eeb

Browse files
authored
feat(auth): Support for creating tenant-scoped session cookies (#467)
* feat(auth): Support for creating tenant-scoped session cookies * fix: Cleaned up the unit test
1 parent 7b99123 commit 3564eeb

11 files changed

+705
-337
lines changed

pom.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@
198198
<plugin>
199199
<groupId>org.jacoco</groupId>
200200
<artifactId>jacoco-maven-plugin</artifactId>
201-
<version>0.7.9</version>
201+
<version>0.8.5</version>
202202
<executions>
203203
<execution>
204204
<id>pre-unit-test</id>
@@ -289,7 +289,7 @@
289289
<!-- Test Phase -->
290290
<plugin>
291291
<artifactId>maven-surefire-plugin</artifactId>
292-
<version>2.19.1</version>
292+
<version>2.22.0</version>
293293
<configuration>
294294
<skipTests>${skipUTs}</skipTests>
295295
</configuration>

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

+185-66
Original file line numberDiff line numberDiff line change
@@ -62,50 +62,13 @@ public abstract class AbstractFirebaseAuth {
6262
private final Supplier<? extends FirebaseUserManager> userManager;
6363
private final JsonFactory jsonFactory;
6464

65-
protected AbstractFirebaseAuth(Builder builder) {
65+
protected AbstractFirebaseAuth(Builder<?> builder) {
6666
this.firebaseApp = checkNotNull(builder.firebaseApp);
6767
this.tokenFactory = threadSafeMemoize(builder.tokenFactory);
6868
this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier);
6969
this.cookieVerifier = threadSafeMemoize(builder.cookieVerifier);
7070
this.userManager = threadSafeMemoize(builder.userManager);
71-
this.jsonFactory = builder.firebaseApp.getOptions().getJsonFactory();
72-
}
73-
74-
protected static Builder builderFromAppAndTenantId(final FirebaseApp app, final String tenantId) {
75-
return AbstractFirebaseAuth.builder()
76-
.setFirebaseApp(app)
77-
.setTokenFactory(
78-
new Supplier<FirebaseTokenFactory>() {
79-
@Override
80-
public FirebaseTokenFactory get() {
81-
return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM, tenantId);
82-
}
83-
})
84-
.setIdTokenVerifier(
85-
new Supplier<FirebaseTokenVerifier>() {
86-
@Override
87-
public FirebaseTokenVerifier get() {
88-
return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM, tenantId);
89-
}
90-
})
91-
.setCookieVerifier(
92-
new Supplier<FirebaseTokenVerifier>() {
93-
@Override
94-
public FirebaseTokenVerifier get() {
95-
return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM);
96-
}
97-
})
98-
.setUserManager(
99-
new Supplier<FirebaseUserManager>() {
100-
@Override
101-
public FirebaseUserManager get() {
102-
return FirebaseUserManager
103-
.builder()
104-
.setFirebaseApp(app)
105-
.setTenantId(tenantId)
106-
.build();
107-
}
108-
});
71+
this.jsonFactory = firebaseApp.getOptions().getJsonFactory();
10972
}
11073

11174
/**
@@ -220,6 +183,51 @@ public String execute() throws FirebaseAuthException {
220183
};
221184
}
222185

186+
/**
187+
* Creates a new Firebase session cookie from the given ID token and options. The returned JWT can
188+
* be set as a server-side session cookie with a custom cookie policy.
189+
*
190+
* @param idToken The Firebase ID token to exchange for a session cookie.
191+
* @param options Additional options required to create the cookie.
192+
* @return A Firebase session cookie string.
193+
* @throws IllegalArgumentException If the ID token is null or empty, or if options is null.
194+
* @throws FirebaseAuthException If an error occurs while generating the session cookie.
195+
*/
196+
public String createSessionCookie(@NonNull String idToken, @NonNull SessionCookieOptions options)
197+
throws FirebaseAuthException {
198+
return createSessionCookieOp(idToken, options).call();
199+
}
200+
201+
/**
202+
* Similar to {@link #createSessionCookie(String, SessionCookieOptions)} but performs the
203+
* operation asynchronously.
204+
*
205+
* @param idToken The Firebase ID token to exchange for a session cookie.
206+
* @param options Additional options required to create the cookie.
207+
* @return An {@code ApiFuture} which will complete successfully with a session cookie string. If
208+
* an error occurs while generating the cookie or if the specified ID token is invalid, the
209+
* future throws a {@link FirebaseAuthException}.
210+
* @throws IllegalArgumentException If the ID token is null or empty, or if options is null.
211+
*/
212+
public ApiFuture<String> createSessionCookieAsync(
213+
@NonNull String idToken, @NonNull SessionCookieOptions options) {
214+
return createSessionCookieOp(idToken, options).callAsync(firebaseApp);
215+
}
216+
217+
private CallableOperation<String, FirebaseAuthException> createSessionCookieOp(
218+
final String idToken, final SessionCookieOptions options) {
219+
checkNotDestroyed();
220+
checkArgument(!Strings.isNullOrEmpty(idToken), "idToken must not be null or empty");
221+
checkNotNull(options, "options must not be null");
222+
final FirebaseUserManager userManager = getUserManager();
223+
return new CallableOperation<String, FirebaseAuthException>() {
224+
@Override
225+
protected String execute() throws FirebaseAuthException {
226+
return userManager.createSessionCookie(idToken, options);
227+
}
228+
};
229+
}
230+
223231
/**
224232
* Parses and verifies a Firebase ID Token.
225233
*
@@ -320,6 +328,87 @@ FirebaseTokenVerifier getIdTokenVerifier(boolean checkRevoked) {
320328
return verifier;
321329
}
322330

331+
/**
332+
* Parses and verifies a Firebase session cookie.
333+
*
334+
* <p>If verified successfully, returns a parsed version of the cookie from which the UID and the
335+
* other claims can be read. If the cookie is invalid, throws a {@link FirebaseAuthException}.
336+
*
337+
* <p>This method does not check whether the cookie has been revoked. See {@link
338+
* #verifySessionCookie(String, boolean)}.
339+
*
340+
* @param cookie A Firebase session cookie string to verify and parse.
341+
* @return A {@link FirebaseToken} representing the verified and decoded cookie.
342+
*/
343+
public FirebaseToken verifySessionCookie(String cookie) throws FirebaseAuthException {
344+
return verifySessionCookie(cookie, false);
345+
}
346+
347+
/**
348+
* Parses and verifies a Firebase session cookie.
349+
*
350+
* <p>If {@code checkRevoked} is true, additionally verifies that the cookie has not been revoked.
351+
*
352+
* <p>If verified successfully, returns a parsed version of the cookie from which the UID and the
353+
* other claims can be read. If the cookie is invalid or has been revoked while {@code
354+
* checkRevoked} is true, throws a {@link FirebaseAuthException}.
355+
*
356+
* @param cookie A Firebase session cookie string to verify and parse.
357+
* @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked.
358+
* @return A {@link FirebaseToken} representing the verified and decoded cookie.
359+
*/
360+
public FirebaseToken verifySessionCookie(String cookie, boolean checkRevoked)
361+
throws FirebaseAuthException {
362+
return verifySessionCookieOp(cookie, checkRevoked).call();
363+
}
364+
365+
/**
366+
* Similar to {@link #verifySessionCookie(String)} but performs the operation asynchronously.
367+
*
368+
* @param cookie A Firebase session cookie string to verify and parse.
369+
* @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or
370+
* unsuccessfully with the failure Exception.
371+
*/
372+
public ApiFuture<FirebaseToken> verifySessionCookieAsync(String cookie) {
373+
return verifySessionCookieAsync(cookie, false);
374+
}
375+
376+
/**
377+
* Similar to {@link #verifySessionCookie(String, boolean)} but performs the operation
378+
* asynchronously.
379+
*
380+
* @param cookie A Firebase session cookie string to verify and parse.
381+
* @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked.
382+
* @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or
383+
* unsuccessfully with the failure Exception.
384+
*/
385+
public ApiFuture<FirebaseToken> verifySessionCookieAsync(String cookie, boolean checkRevoked) {
386+
return verifySessionCookieOp(cookie, checkRevoked).callAsync(firebaseApp);
387+
}
388+
389+
private CallableOperation<FirebaseToken, FirebaseAuthException> verifySessionCookieOp(
390+
final String cookie, final boolean checkRevoked) {
391+
checkNotDestroyed();
392+
checkArgument(!Strings.isNullOrEmpty(cookie), "Session cookie must not be null or empty");
393+
final FirebaseTokenVerifier sessionCookieVerifier = getSessionCookieVerifier(checkRevoked);
394+
return new CallableOperation<FirebaseToken, FirebaseAuthException>() {
395+
@Override
396+
public FirebaseToken execute() throws FirebaseAuthException {
397+
return sessionCookieVerifier.verifyToken(cookie);
398+
}
399+
};
400+
}
401+
402+
@VisibleForTesting
403+
FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) {
404+
FirebaseTokenVerifier verifier = cookieVerifier.get();
405+
if (checkRevoked) {
406+
FirebaseUserManager userManager = getUserManager();
407+
verifier = RevocationCheckDecorator.decorateSessionCookieVerifier(verifier, userManager);
408+
}
409+
return verifier;
410+
}
411+
323412
/**
324413
* Revokes all refresh tokens for the specified user.
325414
*
@@ -1637,19 +1726,11 @@ protected Void execute() throws FirebaseAuthException {
16371726
};
16381727
}
16391728

1640-
FirebaseApp getFirebaseApp() {
1641-
return this.firebaseApp;
1642-
}
1643-
1644-
FirebaseTokenVerifier getCookieVerifier() {
1645-
return this.cookieVerifier.get();
1646-
}
1647-
16481729
FirebaseUserManager getUserManager() {
16491730
return this.userManager.get();
16501731
}
16511732

1652-
protected <T> Supplier<T> threadSafeMemoize(final Supplier<T> supplier) {
1733+
<T> Supplier<T> threadSafeMemoize(final Supplier<T> supplier) {
16531734
return Suppliers.memoize(
16541735
new Supplier<T>() {
16551736
@Override
@@ -1663,7 +1744,7 @@ public T get() {
16631744
});
16641745
}
16651746

1666-
void checkNotDestroyed() {
1747+
private void checkNotDestroyed() {
16671748
synchronized (lock) {
16681749
checkState(
16691750
!destroyed.get(),
@@ -1682,42 +1763,80 @@ final void destroy() {
16821763
/** Performs any additional required clean up. */
16831764
protected abstract void doDestroy();
16841765

1685-
static Builder builder() {
1686-
return new Builder();
1687-
}
1766+
protected abstract static class Builder<T extends Builder<T>> {
16881767

1689-
static class Builder {
1690-
protected FirebaseApp firebaseApp;
1768+
private FirebaseApp firebaseApp;
16911769
private Supplier<FirebaseTokenFactory> tokenFactory;
16921770
private Supplier<? extends FirebaseTokenVerifier> idTokenVerifier;
16931771
private Supplier<? extends FirebaseTokenVerifier> cookieVerifier;
1694-
private Supplier<FirebaseUserManager> userManager;
1772+
private Supplier<? extends FirebaseUserManager> userManager;
16951773

1696-
private Builder() {}
1774+
protected abstract T getThis();
1775+
1776+
public FirebaseApp getFirebaseApp() {
1777+
return firebaseApp;
1778+
}
16971779

1698-
Builder setFirebaseApp(FirebaseApp firebaseApp) {
1780+
public T setFirebaseApp(FirebaseApp firebaseApp) {
16991781
this.firebaseApp = firebaseApp;
1700-
return this;
1782+
return getThis();
17011783
}
17021784

1703-
Builder setTokenFactory(Supplier<FirebaseTokenFactory> tokenFactory) {
1785+
public T setTokenFactory(Supplier<FirebaseTokenFactory> tokenFactory) {
17041786
this.tokenFactory = tokenFactory;
1705-
return this;
1787+
return getThis();
17061788
}
17071789

1708-
Builder setIdTokenVerifier(Supplier<? extends FirebaseTokenVerifier> idTokenVerifier) {
1790+
public T setIdTokenVerifier(Supplier<? extends FirebaseTokenVerifier> idTokenVerifier) {
17091791
this.idTokenVerifier = idTokenVerifier;
1710-
return this;
1792+
return getThis();
17111793
}
17121794

1713-
Builder setCookieVerifier(Supplier<? extends FirebaseTokenVerifier> cookieVerifier) {
1795+
public T setCookieVerifier(Supplier<? extends FirebaseTokenVerifier> cookieVerifier) {
17141796
this.cookieVerifier = cookieVerifier;
1715-
return this;
1797+
return getThis();
17161798
}
17171799

1718-
Builder setUserManager(Supplier<FirebaseUserManager> userManager) {
1800+
public T setUserManager(Supplier<FirebaseUserManager> userManager) {
17191801
this.userManager = userManager;
1720-
return this;
1802+
return getThis();
17211803
}
17221804
}
1805+
1806+
protected static <T extends Builder<T>> T populateBuilderFromApp(
1807+
Builder<T> builder, final FirebaseApp app, @Nullable final String tenantId) {
1808+
return builder.setFirebaseApp(app)
1809+
.setTokenFactory(
1810+
new Supplier<FirebaseTokenFactory>() {
1811+
@Override
1812+
public FirebaseTokenFactory get() {
1813+
return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM, tenantId);
1814+
}
1815+
})
1816+
.setIdTokenVerifier(
1817+
new Supplier<FirebaseTokenVerifier>() {
1818+
@Override
1819+
public FirebaseTokenVerifier get() {
1820+
return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM, tenantId);
1821+
}
1822+
})
1823+
.setCookieVerifier(
1824+
new Supplier<FirebaseTokenVerifier>() {
1825+
@Override
1826+
public FirebaseTokenVerifier get() {
1827+
return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM, tenantId);
1828+
}
1829+
})
1830+
.setUserManager(
1831+
new Supplier<FirebaseUserManager>() {
1832+
@Override
1833+
public FirebaseUserManager get() {
1834+
return FirebaseUserManager
1835+
.builder()
1836+
.setFirebaseApp(app)
1837+
.setTenantId(tenantId)
1838+
.build();
1839+
}
1840+
});
1841+
}
17231842
}

0 commit comments

Comments
 (0)