Skip to content

Commit acec5fe

Browse files
authored
Custom Claims and List Users Support (#92)
* Implemented ThreadManager API for configuring the thread pools and thread factories used by the SDK * Giving all threads unique names; Updated documentation; Using daemons in default thread managers to ensure clean JVM exit * Updated comments and documentation * Adding tests for options * Test cases for basic ThreadManager API * More test cases * Made the executor service private in FirebaseApp; Refactored the tests for clarity * Clean separation of long-lived and short-lived tasks of the SDK * Updated documentation; More tests; Starting token refresher from database. * Updated documentation and log statements * Removing test file * Initializing executor in FirebaseApp constructor. Minor improvements to documentation and tests. * Fixed token refresher stop() logic * Updated documentation; Renamed submit() to submitCallable() and other minor changes * Implemented custom claims support * More test cases * Initial list users implementation * Changed the setCustomClaims() API on UpdateRequest; Deferring the size check until we have to serialize the update parameters * Cleaned up the listUser() impl * Further developed the listUsers() API * Updated tests and documentation * Updated documentation * Implementing batch get operation for users * Further cleaned up the iterator code * Further cleaned up the iterator code * Updated list user API * Implementing listUsers() using the gax Page interface * Code clean up and tests * Updated documentation * Removed renamed test file * Adding new line at end of file * Adding some annotations * Using empty map in UserRecord when no claims are present; Updated documentation and tests * minor improvements * Removing RPC call from constructor
1 parent cac3f7d commit acec5fe

14 files changed

+1516
-40
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 com.google.firebase.auth.internal.DownloadAccountResponse.User;
20+
import com.google.firebase.internal.Nullable;
21+
22+
/**
23+
* Contains metadata associated with a Firebase user account, along with password hash and salt.
24+
* Instances of this class are immutable and thread-safe.
25+
*/
26+
public class ExportedUserRecord extends UserRecord {
27+
28+
private final String passwordHash;
29+
private final String passwordSalt;
30+
31+
ExportedUserRecord(User response) {
32+
super(response);
33+
this.passwordHash = response.getPasswordHash();
34+
this.passwordSalt = response.getPasswordSalt();
35+
}
36+
37+
/**
38+
* Returns the user's password hash as a base64-encoded string.
39+
*
40+
* <p>If the Firebase Auth hashing algorithm (SCRYPT) was used to create the user account,
41+
* returns the base64-encoded password hash of the user. If a different hashing algorithm was
42+
* used to create this user, as is typical when migrating from another Auth system, returns
43+
* an empty string. Returns null if no password is set.
44+
*
45+
* @return A base64-encoded password hash, possibly empty or null.
46+
*/
47+
@Nullable
48+
public String getPasswordHash() {
49+
return passwordHash;
50+
}
51+
52+
/**
53+
* Returns the user's password salt as a base64-encoded string.
54+
*
55+
* <p>If the Firebase Auth hashing algorithm (SCRYPT) was used to create the user account,
56+
* returns the base64-encoded password salt of the user. If a different hashing algorithm was
57+
* used to create this user, as is typical when migrating from another Auth system, returns
58+
* an empty string. Returns null if no password is set.
59+
*
60+
* @return A base64-encoded password salt, possibly empty or null.
61+
*/
62+
@Nullable
63+
public String getPasswordSalt() {
64+
return passwordSalt;
65+
}
66+
}

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

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,14 @@
3030
import com.google.common.base.Strings;
3131
import com.google.firebase.FirebaseApp;
3232
import com.google.firebase.ImplFirebaseTrampolines;
33+
import com.google.firebase.auth.ListUsersPage.DefaultUserSource;
34+
import com.google.firebase.auth.ListUsersPage.PageFactory;
3335
import com.google.firebase.auth.UserRecord.CreateRequest;
3436
import com.google.firebase.auth.UserRecord.UpdateRequest;
3537
import com.google.firebase.auth.internal.FirebaseTokenFactory;
3638
import com.google.firebase.auth.internal.FirebaseTokenVerifier;
3739
import com.google.firebase.internal.FirebaseService;
40+
import com.google.firebase.internal.Nullable;
3841
import com.google.firebase.internal.TaskToApiFuture;
3942
import com.google.firebase.tasks.Task;
4043

@@ -346,6 +349,46 @@ public ApiFuture<UserRecord> getUserByPhoneNumberAsync(final String phoneNumber)
346349
return new TaskToApiFuture<>(getUserByPhoneNumber(phoneNumber));
347350
}
348351

352+
private Task<ListUsersPage> listUsers(@Nullable String pageToken, int maxResults) {
353+
checkNotDestroyed();
354+
final PageFactory factory = new PageFactory(
355+
new DefaultUserSource(userManager), maxResults, pageToken);
356+
return call(new Callable<ListUsersPage>() {
357+
@Override
358+
public ListUsersPage call() throws Exception {
359+
return factory.create();
360+
}
361+
});
362+
}
363+
364+
/**
365+
* Gets a page of users starting from the specified {@code pageToken}. Page size will be
366+
* limited to 1000 users.
367+
*
368+
* @param pageToken A non-empty page token string, or null to retrieve the first page of users.
369+
* @return An {@code ApiFuture} which will complete successfully with a {@link ListUsersPage}
370+
* instance. If an error occurs while retrieving user data, the future throws an exception.
371+
* @throws IllegalArgumentException If the specified page token is empty.
372+
*/
373+
public ApiFuture<ListUsersPage> listUsersAsync(@Nullable String pageToken) {
374+
return listUsersAsync(pageToken, FirebaseUserManager.MAX_LIST_USERS_RESULTS);
375+
}
376+
377+
/**
378+
* Gets a page of users starting from the specified {@code pageToken}.
379+
*
380+
* @param pageToken A non-empty page token string, or null to retrieve the first page of users.
381+
* @param maxResults Maximum number of users to include in the returned page. This may not
382+
* exceed 1000.
383+
* @return An {@code ApiFuture} which will complete successfully with a {@link ListUsersPage}
384+
* instance. If an error occurs while retrieving user data, the future throws an exception.
385+
* @throws IllegalArgumentException If the specified page token is empty, or max results value
386+
* is invalid.
387+
*/
388+
public ApiFuture<ListUsersPage> listUsersAsync(@Nullable String pageToken, int maxResults) {
389+
return new TaskToApiFuture<>(listUsers(pageToken, maxResults));
390+
}
391+
349392
/**
350393
* Similar to {@link #createUserAsync(CreateRequest)}, but returns a {@link Task}.
351394
*
@@ -398,7 +441,7 @@ public Task<UserRecord> updateUser(final UpdateRequest request) {
398441
return call(new Callable<UserRecord>() {
399442
@Override
400443
public UserRecord call() throws Exception {
401-
userManager.updateUser(request);
444+
userManager.updateUser(request, jsonFactory);
402445
return userManager.getUserById(request.getUid());
403446
}
404447
});
@@ -418,6 +461,35 @@ public ApiFuture<UserRecord> updateUserAsync(final UpdateRequest request) {
418461
return new TaskToApiFuture<>(updateUser(request));
419462
}
420463

464+
private Task<Void> setCustomClaims(String uid, Map<String, Object> claims) {
465+
checkNotDestroyed();
466+
final UpdateRequest request = new UpdateRequest(uid).setCustomClaims(claims);
467+
return call(new Callable<Void>() {
468+
@Override
469+
public Void call() throws Exception {
470+
userManager.updateUser(request, jsonFactory);
471+
return null;
472+
}
473+
});
474+
}
475+
476+
/**
477+
* Sets the specified custom claims on an existing user account. A null claims value removes
478+
* any claims currently set on the user account. The claims should serialize into a valid JSON
479+
* string. The serialized claims must not be larger than 1000 characters.
480+
*
481+
* @param uid A user ID string.
482+
* @param claims A map of custom claims or null.
483+
* @return An {@code ApiFuture} which will complete successfully when the user account has been
484+
* updated. If an error occurs while deleting the user account, the future throws a
485+
* {@link FirebaseAuthException}.
486+
* @throws IllegalArgumentException If the user ID string is null or empty, or the claims
487+
* payload is invalid or too large.
488+
*/
489+
public ApiFuture<Void> setCustomClaimsAsync(String uid, Map<String, Object> claims) {
490+
return new TaskToApiFuture<>(setCustomClaims(uid, claims));
491+
}
492+
421493
/**
422494
* Similar to {@link #deleteUserAsync(String)}, but returns a {@link Task}.
423495
*
@@ -440,19 +512,6 @@ public Void call() throws Exception {
440512
});
441513
}
442514

443-
private void checkNotDestroyed() {
444-
synchronized (lock) {
445-
checkState(!destroyed.get(), "FirebaseAuth instance is no longer alive. This happens when "
446-
+ "the parent FirebaseApp instance has been deleted.");
447-
}
448-
}
449-
450-
private void destroy() {
451-
synchronized (lock) {
452-
destroyed.set(true);
453-
}
454-
}
455-
456515
/**
457516
* Deletes the user identified by the specified user ID.
458517
*
@@ -470,6 +529,19 @@ private <T> Task<T> call(Callable<T> command) {
470529
return ImplFirebaseTrampolines.submitCallable(firebaseApp, command);
471530
}
472531

532+
private void checkNotDestroyed() {
533+
synchronized (lock) {
534+
checkState(!destroyed.get(), "FirebaseAuth instance is no longer alive. This happens when "
535+
+ "the parent FirebaseApp instance has been deleted.");
536+
}
537+
}
538+
539+
private void destroy() {
540+
synchronized (lock) {
541+
destroyed.set(true);
542+
}
543+
}
544+
473545
private static final String SERVICE_ID = FirebaseAuth.class.getName();
474546

475547
private static class FirebaseAuthService extends FirebaseService<FirebaseAuth> {

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

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,12 @@
3737
import com.google.common.collect.ImmutableMap;
3838
import com.google.firebase.auth.UserRecord.CreateRequest;
3939
import com.google.firebase.auth.UserRecord.UpdateRequest;
40+
import com.google.firebase.auth.internal.DownloadAccountResponse;
4041
import com.google.firebase.auth.internal.GetAccountInfoResponse;
4142

4243
import com.google.firebase.internal.SdkUtils;
4344
import java.io.IOException;
45+
import java.util.List;
4446
import java.util.Map;
4547

4648
/**
@@ -56,8 +58,15 @@ class FirebaseUserManager {
5658
static final String USER_CREATE_ERROR = "USER_CREATE_ERROR";
5759
static final String USER_UPDATE_ERROR = "USER_UPDATE_ERROR";
5860
static final String USER_DELETE_ERROR = "USER_DELETE_ERROR";
61+
static final String LIST_USERS_ERROR = "LIST_USERS_ERROR";
5962
static final String INTERNAL_ERROR = "INTERNAL_ERROR";
6063

64+
static final int MAX_LIST_USERS_RESULTS = 1000;
65+
66+
static final List<String> RESERVED_CLAIMS = ImmutableList.of(
67+
"amr", "at_hash", "aud", "auth_time", "azp", "cnf", "c_hash", "exp", "iat",
68+
"iss", "jti", "nbf", "nonce", "sub", "firebase");
69+
6170
private static final String ID_TOOLKIT_URL =
6271
"https://www.googleapis.com/identitytoolkit/v3/relyingparty/";
6372
private static final String CLIENT_VERSION_HEADER = "X-Client-Version";
@@ -157,10 +166,10 @@ String createUser(CreateRequest request) throws FirebaseAuthException {
157166
throw new FirebaseAuthException(USER_CREATE_ERROR, "Failed to create new user");
158167
}
159168

160-
void updateUser(UpdateRequest request) throws FirebaseAuthException {
169+
void updateUser(UpdateRequest request, JsonFactory jsonFactory) throws FirebaseAuthException {
161170
GenericJson response;
162171
try {
163-
response = post("setAccountInfo", request.getProperties(), GenericJson.class);
172+
response = post("setAccountInfo", request.getProperties(jsonFactory), GenericJson.class);
164173
} catch (IOException e) {
165174
throw new FirebaseAuthException(USER_UPDATE_ERROR,
166175
"IO error while updating user: " + request.getUid(), e);
@@ -187,6 +196,28 @@ void deleteUser(String uid) throws FirebaseAuthException {
187196
}
188197
}
189198

199+
DownloadAccountResponse listUsers(int maxResults, String pageToken) throws FirebaseAuthException {
200+
ImmutableMap.Builder<String, Object> builder = ImmutableMap.<String, Object>builder()
201+
.put("maxResults", maxResults);
202+
if (pageToken != null) {
203+
checkArgument(!pageToken.equals(ListUsersPage.END_OF_LIST), "invalid end of list page token");
204+
builder.put("nextPageToken", pageToken);
205+
}
206+
207+
DownloadAccountResponse response;
208+
try {
209+
response = post("downloadAccount", builder.build(), DownloadAccountResponse.class);
210+
if (response == null) {
211+
throw new FirebaseAuthException(LIST_USERS_ERROR,
212+
"Unexpected response from download user account API.");
213+
}
214+
return response;
215+
} catch (IOException e) {
216+
throw new FirebaseAuthException(LIST_USERS_ERROR,
217+
"IO error while downloading user accounts.", e);
218+
}
219+
}
220+
190221
private <T> T post(String path, Object content, Class<T> clazz) throws IOException {
191222
checkArgument(!Strings.isNullOrEmpty(path), "path must not be null or empty");
192223
checkNotNull(content, "content must not be null");

0 commit comments

Comments
 (0)