Skip to content

Commit 6baace6

Browse files
authored
Session Cookie Management API (firebase#161)
* Added createSessionCookie() method * Fixed lint error * Added cookie verification logic * Added snippets and updated changelog * Minor refactoring of internal APIs * Adding missing newline at eof * Fixing a verification check * Responding to code review comments; Using String.format() to construct error messages; Updated documentation and other readability improvements * Renamed helper method
1 parent a850e7f commit 6baace6

13 files changed

Lines changed: 952 additions & 155 deletions

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
# Unreleased
22

3+
- [added] A new `FirebaseAuth.createSessionCookieAsync()` method for
4+
creating a long-lived session cookie given a valid ID token.
5+
- [added] A new `FirebaseAuth.verifySessionCookieAsync()` method for
6+
verifying a given cookie string is valid.
37
- [fixed] Upgraded Cloud Firestore dependency version to 0.44.0-beta.
48
- [fixed] Upgraded Cloud Storage dependency version to 1.26.0.
59
- [fixed] Upgraded Netty dependency version to 4.1.22.
610

711
# v5.10.0
812

9-
13+
- [fixed] Using the `HttpTransport` specified at `FirebaseOptions` in
14+
`GooglePublicKeysManager`. This enables developers to use a custom
15+
transport to fetch public keys when verifying ID tokens and session
16+
cookies.
1017
- [added] Connection timeout and read timeout for HTTP/REST connections
1118
can now be configured via `FirebaseOptions.Builder` at app
1219
initialization.

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,5 +471,12 @@
471471
<version>4.12</version>
472472
<scope>test</scope>
473473
</dependency>
474+
<dependency>
475+
<!-- Used for some snippets -->
476+
<groupId>javax.ws.rs</groupId>
477+
<artifactId>javax.ws.rs-api</artifactId>
478+
<version>2.0</version>
479+
<scope>test</scope>
480+
</dependency>
474481
</dependencies>
475482
</project>

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

Lines changed: 111 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import static com.google.common.base.Preconditions.checkNotNull;
2121
import static com.google.common.base.Preconditions.checkState;
2222

23-
import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager;
2423
import com.google.api.client.json.JsonFactory;
2524
import com.google.api.client.util.Clock;
2625
import com.google.api.core.ApiFuture;
@@ -36,7 +35,9 @@
3635
import com.google.firebase.auth.UserRecord.UpdateRequest;
3736
import com.google.firebase.auth.internal.FirebaseTokenFactory;
3837
import com.google.firebase.auth.internal.FirebaseTokenVerifier;
38+
import com.google.firebase.auth.internal.KeyManagers;
3939
import com.google.firebase.internal.FirebaseService;
40+
import com.google.firebase.internal.NonNull;
4041
import com.google.firebase.internal.Nullable;
4142
import com.google.firebase.internal.TaskToApiFuture;
4243
import com.google.firebase.tasks.Task;
@@ -54,10 +55,10 @@
5455
*/
5556
public class FirebaseAuth {
5657

57-
private final GooglePublicKeysManager googlePublicKeysManager;
5858
private final Clock clock;
5959

6060
private final FirebaseApp firebaseApp;
61+
private final KeyManagers keyManagers;
6162
private final GoogleCredentials credentials;
6263
private final String projectId;
6364
private final JsonFactory jsonFactory;
@@ -66,21 +67,17 @@ public class FirebaseAuth {
6667
private final Object lock;
6768

6869
private FirebaseAuth(FirebaseApp firebaseApp) {
69-
this(firebaseApp,
70-
FirebaseTokenVerifier.buildGooglePublicKeysManager(
71-
firebaseApp.getOptions().getHttpTransport()),
72-
Clock.SYSTEM);
70+
this(firebaseApp, KeyManagers.getDefault(firebaseApp, Clock.SYSTEM), Clock.SYSTEM);
7371
}
7472

7573
/**
7674
* Constructor for injecting a GooglePublicKeysManager, which is used to verify tokens are
7775
* correctly signed. This should only be used for testing to override the default key manager.
7876
*/
7977
@VisibleForTesting
80-
FirebaseAuth(
81-
FirebaseApp firebaseApp, GooglePublicKeysManager googlePublicKeysManager, Clock clock) {
78+
FirebaseAuth(FirebaseApp firebaseApp, KeyManagers keyManagers, Clock clock) {
8279
this.firebaseApp = checkNotNull(firebaseApp);
83-
this.googlePublicKeysManager = checkNotNull(googlePublicKeysManager);
80+
this.keyManagers = checkNotNull(keyManagers);
8481
this.clock = checkNotNull(clock);
8582
this.credentials = ImplFirebaseTrampolines.getCredentials(firebaseApp);
8683
this.projectId = ImplFirebaseTrampolines.getProjectId(firebaseApp);
@@ -114,6 +111,106 @@ public static synchronized FirebaseAuth getInstance(FirebaseApp app) {
114111
return service.getInstance();
115112
}
116113

114+
private Task<String> createSessionCookie(
115+
final String idToken, final SessionCookieOptions options) {
116+
checkNotDestroyed();
117+
checkArgument(!Strings.isNullOrEmpty(idToken), "idToken must not be null or empty");
118+
checkNotNull(options, "options must not be null");
119+
return call(new Callable<String>() {
120+
@Override
121+
public String call() throws Exception {
122+
return userManager.createSessionCookie(idToken, options);
123+
}
124+
});
125+
}
126+
127+
/**
128+
* Creates a new Firebase session cookie from the given ID token and options. The returned JWT
129+
* can be set as a server-side session cookie with a custom cookie policy.
130+
*
131+
* @param idToken The Firebase ID token to exchange for a session cookie.
132+
* @param options Additional options required to create the cookie.
133+
* @return An {@code ApiFuture} which will complete successfully with a session cookie string.
134+
* If an error occurs while generating the cookie or if the specified ID token is invalid,
135+
* the future throws a {@link FirebaseAuthException}.
136+
* @throws IllegalArgumentException If the ID token is null or empty, or if options is null.
137+
*/
138+
public ApiFuture<String> createSessionCookieAsync(
139+
@NonNull String idToken, @NonNull SessionCookieOptions options) {
140+
return new TaskToApiFuture<>(createSessionCookie(idToken, options));
141+
}
142+
143+
/**
144+
* Parses and verifies a Firebase session cookie.
145+
*
146+
* <p>If verified successfully, the returned {@code ApiFuture} completes with a parsed version of
147+
* the cookie from which the UID and the other claims can be read. If the cookie is invalid,
148+
* the future throws an exception indicating the failure.
149+
*
150+
* <p>This method does not check whether the cookie has been revoked. See
151+
* {@link #verifySessionCookieAsync(String, boolean)}.
152+
*
153+
* @param cookie A Firebase session cookie string to verify and parse.
154+
* @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or
155+
* unsuccessfully with the failure Exception.
156+
*/
157+
public ApiFuture<FirebaseToken> verifySessionCookieAsync(String cookie) {
158+
return new TaskToApiFuture<>(verifySessionCookie(cookie, false));
159+
}
160+
161+
/**
162+
* Parses and verifies a Firebase session cookie.
163+
*
164+
* <p>If {@code checkRevoked} is true, additionally verifies that the cookie has not been
165+
* revoked.
166+
*
167+
* <p>If verified successfully, the returned {@code ApiFuture} completes with a parsed version of
168+
* the cookie from which the UID and the other claims can be read. If the cookie is invalid or
169+
* has been revoked while {@code checkRevoked} is true, the future throws an exception indicating
170+
* the failure.
171+
*
172+
* @param cookie A Firebase session cookie string to verify and parse.
173+
* @param checkRevoked A boolean indicating whether to check if the cookie was explicitly
174+
* revoked.
175+
* @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or
176+
* unsuccessfully with the failure Exception.
177+
*/
178+
public ApiFuture<FirebaseToken> verifySessionCookieAsync(String cookie, boolean checkRevoked) {
179+
return new TaskToApiFuture<>(verifySessionCookie(cookie, checkRevoked));
180+
}
181+
182+
private Task<FirebaseToken> verifySessionCookie(final String cookie, final boolean checkRevoked) {
183+
checkNotDestroyed();
184+
checkState(!Strings.isNullOrEmpty(projectId),
185+
"Must initialize FirebaseApp with a project ID to call verifySessionCookie()");
186+
return call(new Callable<FirebaseToken>() {
187+
@Override
188+
public FirebaseToken call() throws Exception {
189+
FirebaseTokenVerifier firebaseTokenVerifier =
190+
FirebaseTokenVerifier.createSessionCookieVerifier(projectId, keyManagers, clock);
191+
FirebaseToken firebaseToken = FirebaseToken.parse(jsonFactory, cookie);
192+
// This will throw a FirebaseAuthException with details on how the token is invalid.
193+
firebaseTokenVerifier.verifyTokenAndSignature(firebaseToken.getToken());
194+
195+
if (checkRevoked) {
196+
checkRevoked(firebaseToken, "session cookie",
197+
FirebaseUserManager.SESSION_COOKIE_REVOKED_ERROR);
198+
}
199+
return firebaseToken;
200+
}
201+
});
202+
}
203+
204+
private void checkRevoked(
205+
FirebaseToken firebaseToken, String label, String errorCode) throws FirebaseAuthException {
206+
String uid = firebaseToken.getUid();
207+
UserRecord user = userManager.getUserById(uid);
208+
long issuedAt = (long) firebaseToken.getClaims().get("iat");
209+
if (user.getTokensValidAfterTimestamp() > issuedAt * 1000) {
210+
throw new FirebaseAuthException(errorCode, "Firebase " + label + " revoked");
211+
}
212+
}
213+
117214
/**
118215
* Similar to {@link #createCustomTokenAsync(String)}, but returns a {@link Task}.
119216
*
@@ -212,27 +309,14 @@ private Task<FirebaseToken> verifyIdToken(final String token, final boolean chec
212309
return call(new Callable<FirebaseToken>() {
213310
@Override
214311
public FirebaseToken call() throws Exception {
215-
216312
FirebaseTokenVerifier firebaseTokenVerifier =
217-
new FirebaseTokenVerifier.Builder()
218-
.setProjectId(projectId)
219-
.setPublicKeysManager(googlePublicKeysManager)
220-
.setClock(clock)
221-
.build();
222-
313+
FirebaseTokenVerifier.createIdTokenVerifier(projectId, keyManagers, clock);
223314
FirebaseToken firebaseToken = FirebaseToken.parse(jsonFactory, token);
224-
225315
// This will throw a FirebaseAuthException with details on how the token is invalid.
226316
firebaseTokenVerifier.verifyTokenAndSignature(firebaseToken.getToken());
227-
317+
228318
if (checkRevoked) {
229-
String uid = firebaseToken.getUid();
230-
UserRecord user = userManager.getUserById(uid);
231-
long issuedAt = (long) firebaseToken.getClaims().get("iat");
232-
if (user.getTokensValidAfterTimestamp() > issuedAt * 1000) {
233-
throw new FirebaseAuthException(FirebaseUserManager.ID_TOKEN_REVOKED_ERROR,
234-
"Firebase auth token revoked");
235-
}
319+
checkRevoked(firebaseToken, "auth token", FirebaseUserManager.ID_TOKEN_REVOKED_ERROR);
236320
}
237321
return firebaseToken;
238322
}
@@ -241,7 +325,7 @@ public FirebaseToken call() throws Exception {
241325

242326
private Task<Void> revokeRefreshTokens(String uid) {
243327
checkNotDestroyed();
244-
int currentTimeSeconds = (int) (System.currentTimeMillis() / 1000);
328+
long currentTimeSeconds = System.currentTimeMillis() / 1000L;
245329
final UpdateRequest request = new UpdateRequest(uid).setValidSince(currentTimeSeconds);
246330
return call(new Callable<Void>() {
247331
@Override
@@ -320,7 +404,7 @@ public ApiFuture<FirebaseToken> verifyIdTokenAsync(final String token) {
320404
* failure.
321405
*
322406
* @param token A Firebase ID Token to verify and parse.
323-
* @param checkRevoked A boolean denoting whether to check if the tokens were revoked.
407+
* @param checkRevoked A boolean indicating whether to check if the tokens were revoked.
324408
* @return An {@code ApiFuture} which will complete successfully with the parsed token, or
325409
* unsuccessfully with the failure Exception.
326410
*/

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class FirebaseUserManager {
6060
static final String USER_NOT_FOUND_ERROR = "user-not-found";
6161
static final String INTERNAL_ERROR = "internal-error";
6262
static final String ID_TOKEN_REVOKED_ERROR = "id-token-revoked";
63+
static final String SESSION_COOKIE_REVOKED_ERROR = "session-cookie-revoked";
6364

6465
// Map of server-side error codes to SDK error codes.
6566
// SDK error codes defined at: https://firebase.google.com/docs/auth/admin/errors
@@ -194,6 +195,20 @@ DownloadAccountResponse listUsers(int maxResults, String pageToken) throws Fireb
194195
return response;
195196
}
196197

198+
String createSessionCookie(String idToken,
199+
SessionCookieOptions options) throws FirebaseAuthException {
200+
final Map<String, Object> payload = ImmutableMap.<String, Object>of(
201+
"idToken", idToken, "validDuration", options.getExpiresInSeconds());
202+
GenericJson response = post("createSessionCookie", payload, GenericJson.class);
203+
if (response != null) {
204+
String cookie = (String) response.get("sessionCookie");
205+
if (!Strings.isNullOrEmpty(cookie)) {
206+
return cookie;
207+
}
208+
}
209+
throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create session cookie");
210+
}
211+
197212
private <T> T post(String path, Object content, Class<T> clazz) throws FirebaseAuthException {
198213
checkArgument(!Strings.isNullOrEmpty(path), "path must not be null or empty");
199214
checkNotNull(content, "content must not be null");
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2017 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.auth;
18+
19+
import static com.google.common.base.Preconditions.checkArgument;
20+
21+
import java.util.concurrent.TimeUnit;
22+
23+
/**
24+
* A set of additional options that can be passed to
25+
* {@link FirebaseAuth#createSessionCookieAsync(String, SessionCookieOptions)}.
26+
*/
27+
public class SessionCookieOptions {
28+
29+
private final long expiresIn;
30+
31+
private SessionCookieOptions(Builder builder) {
32+
checkArgument(builder.expiresIn > TimeUnit.MINUTES.toMillis(5),
33+
"expiresIn duration must be at least 5 minutes");
34+
checkArgument(builder.expiresIn < TimeUnit.DAYS.toMillis(14),
35+
"expiresIn duration must be at most 14 days");
36+
this.expiresIn = builder.expiresIn;
37+
}
38+
39+
long getExpiresInSeconds() {
40+
return TimeUnit.MILLISECONDS.toSeconds(expiresIn);
41+
}
42+
43+
/**
44+
* Creates a new {@link Builder}.
45+
*/
46+
public static Builder builder() {
47+
return new Builder();
48+
}
49+
50+
public static class Builder {
51+
52+
private long expiresIn;
53+
54+
private Builder() {}
55+
56+
/**
57+
* Sets the duration until the cookie is expired in milliseconds. Must be between 5 minutes
58+
* and 14 days.
59+
*
60+
* @param expiresInMillis Time duration in milliseconds.
61+
* @return This builder.
62+
*/
63+
public Builder setExpiresIn(long expiresInMillis) {
64+
this.expiresIn = expiresInMillis;
65+
return this;
66+
}
67+
68+
/**
69+
* Creates a new {@link SessionCookieOptions} instance.
70+
*/
71+
public SessionCookieOptions build() {
72+
return new SessionCookieOptions(this);
73+
}
74+
}
75+
76+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public String createSignedCustomAuthTokenForUser(
8383
for (String key : developerClaims.keySet()) {
8484
if (reservedNames.contains(key)) {
8585
throw new IllegalArgumentException(
86-
String.format("developer_claims can not contain a reserved key: %s", key));
86+
String.format("developerClaims must not contain a reserved key: %s", key));
8787
}
8888
}
8989
GenericJson jsonObject = new GenericJson();

0 commit comments

Comments
 (0)