Skip to content

Custom Claims and List Users Support #92

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 41 commits into from
Dec 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a2ee38c
Implemented ThreadManager API for configuring the thread pools and th…
hiranya911 Sep 16, 2017
1cf4c3a
Giving all threads unique names; Updated documentation; Using daemons…
hiranya911 Sep 16, 2017
8234ca1
Updated comments and documentation
hiranya911 Sep 17, 2017
a8fb84e
Adding tests for options
hiranya911 Sep 18, 2017
913fda5
Test cases for basic ThreadManager API
hiranya911 Sep 18, 2017
c218f82
More test cases
hiranya911 Sep 18, 2017
a803a4e
Made the executor service private in FirebaseApp; Refactored the test…
hiranya911 Sep 21, 2017
458d0c2
Clean separation of long-lived and short-lived tasks of the SDK
hiranya911 Sep 23, 2017
b63b17a
Updated documentation; More tests; Starting token refresher from data…
hiranya911 Sep 24, 2017
050bfa4
Updated documentation and log statements
hiranya911 Sep 25, 2017
f06b9b3
Removing test file
hiranya911 Sep 25, 2017
846c93c
Initializing executor in FirebaseApp constructor. Minor improvements …
hiranya911 Sep 26, 2017
d3ea8e9
Merged with the latest ThreadManager impl
hiranya911 Sep 26, 2017
e06ae1d
Merge branch 'hkj-exec-cleanup' of github.com:firebase/firebase-admin…
hiranya911 Sep 26, 2017
dfca1e7
Fixed token refresher stop() logic
hiranya911 Sep 26, 2017
b310b0a
Updated documentation; Renamed submit() to submitCallable() and other…
hiranya911 Sep 28, 2017
ae32b93
Merging with latest base
hiranya911 Sep 28, 2017
6c6d44b
Implemented custom claims support
hiranya911 Oct 3, 2017
6ecc866
More test cases
hiranya911 Oct 4, 2017
de99eae
Initial list users implementation
hiranya911 Oct 4, 2017
f58d41f
Changed the setCustomClaims() API on UpdateRequest; Deferring the siz…
hiranya911 Oct 4, 2017
a5977be
Cleaned up the listUser() impl
hiranya911 Oct 4, 2017
ddd68d0
Further developed the listUsers() API
hiranya911 Oct 4, 2017
3a2478b
Updated tests and documentation
hiranya911 Oct 4, 2017
770e63b
Updated documentation
hiranya911 Oct 5, 2017
7df8719
Merged with master
hiranya911 Oct 7, 2017
4907345
Implementing batch get operation for users
hiranya911 Oct 7, 2017
a841dc1
Further cleaned up the iterator code
hiranya911 Oct 7, 2017
5f2c906
Further cleaned up the iterator code
hiranya911 Oct 7, 2017
7a0ca59
Updated list user API
hiranya911 Oct 7, 2017
4a255c6
Implementing listUsers() using the gax Page interface
hiranya911 Oct 24, 2017
a394ec1
Merged with master
hiranya911 Oct 24, 2017
0a76f43
Code clean up and tests
hiranya911 Oct 24, 2017
5d66314
Updated documentation
hiranya911 Oct 24, 2017
d5fb40a
Removed renamed test file
hiranya911 Oct 31, 2017
40bca6c
Adding new line at end of file
hiranya911 Oct 31, 2017
5fd9b4c
Adding some annotations
hiranya911 Oct 31, 2017
6d4af29
Merge branch 'master' into hkj-custom-claims
hiranya911 Nov 8, 2017
95ca61e
Using empty map in UserRecord when no claims are present; Updated doc…
hiranya911 Nov 9, 2017
01460e0
minor improvements
hiranya911 Nov 9, 2017
db05642
Removing RPC call from constructor
hiranya911 Nov 9, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/main/java/com/google/firebase/auth/ExportedUserRecord.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2017 Google Inc.
*
* 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.firebase.auth.internal.DownloadAccountResponse.User;
import com.google.firebase.internal.Nullable;

/**
* Contains metadata associated with a Firebase user account, along with password hash and salt.
* Instances of this class are immutable and thread-safe.
*/
public class ExportedUserRecord extends UserRecord {

private final String passwordHash;
private final String passwordSalt;

ExportedUserRecord(User response) {
super(response);
this.passwordHash = response.getPasswordHash();
this.passwordSalt = response.getPasswordSalt();
}

/**
* Returns the user's password hash as a base64-encoded string.
*
* <p>If the Firebase Auth hashing algorithm (SCRYPT) was used to create the user account,
* returns the base64-encoded password hash of the user. If a different hashing algorithm was
* used to create this user, as is typical when migrating from another Auth system, returns
* an empty string. Returns null if no password is set.
*
* @return A base64-encoded password hash, possibly empty or null.
*/
@Nullable
public String getPasswordHash() {
return passwordHash;
}

/**
* Returns the user's password salt as a base64-encoded string.
*
* <p>If the Firebase Auth hashing algorithm (SCRYPT) was used to create the user account,
* returns the base64-encoded password salt of the user. If a different hashing algorithm was
* used to create this user, as is typical when migrating from another Auth system, returns
* an empty string. Returns null if no password is set.
*
* @return A base64-encoded password salt, possibly empty or null.
*/
@Nullable
public String getPasswordSalt() {
return passwordSalt;
}
}
100 changes: 86 additions & 14 deletions src/main/java/com/google/firebase/auth/FirebaseAuth.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@
import com.google.common.base.Strings;
import com.google.firebase.FirebaseApp;
import com.google.firebase.ImplFirebaseTrampolines;
import com.google.firebase.auth.ListUsersPage.DefaultUserSource;
import com.google.firebase.auth.ListUsersPage.PageFactory;
import com.google.firebase.auth.UserRecord.CreateRequest;
import com.google.firebase.auth.UserRecord.UpdateRequest;
import com.google.firebase.auth.internal.FirebaseTokenFactory;
import com.google.firebase.auth.internal.FirebaseTokenVerifier;
import com.google.firebase.internal.FirebaseService;
import com.google.firebase.internal.Nullable;
import com.google.firebase.internal.TaskToApiFuture;
import com.google.firebase.tasks.Task;

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

private Task<ListUsersPage> listUsers(@Nullable String pageToken, int maxResults) {
checkNotDestroyed();
final PageFactory factory = new PageFactory(
new DefaultUserSource(userManager), maxResults, pageToken);
return call(new Callable<ListUsersPage>() {
@Override
public ListUsersPage call() throws Exception {
return factory.create();
}
});
}

/**
* Gets a page of users starting from the specified {@code pageToken}. Page size will be
* limited to 1000 users.
*
* @param pageToken A non-empty page token string, or null to retrieve the first page of users.
* @return An {@code ApiFuture} which will complete successfully with a {@link ListUsersPage}
* instance. If an error occurs while retrieving user data, the future throws an exception.
* @throws IllegalArgumentException If the specified page token is empty.
*/
public ApiFuture<ListUsersPage> listUsersAsync(@Nullable String pageToken) {
return listUsersAsync(pageToken, FirebaseUserManager.MAX_LIST_USERS_RESULTS);
}

/**
* Gets a page of users starting from the specified {@code pageToken}.
*
* @param pageToken A non-empty page token string, or null to retrieve the first page of users.
* @param maxResults Maximum number of users to include in the returned page. This may not
* exceed 1000.
* @return An {@code ApiFuture} which will complete successfully with a {@link ListUsersPage}
* instance. If an error occurs while retrieving user data, the future throws an exception.
* @throws IllegalArgumentException If the specified page token is empty, or max results value
* is invalid.
*/
public ApiFuture<ListUsersPage> listUsersAsync(@Nullable String pageToken, int maxResults) {
return new TaskToApiFuture<>(listUsers(pageToken, maxResults));
}

/**
* Similar to {@link #createUserAsync(CreateRequest)}, but returns a {@link Task}.
*
Expand Down Expand Up @@ -398,7 +441,7 @@ public Task<UserRecord> updateUser(final UpdateRequest request) {
return call(new Callable<UserRecord>() {
@Override
public UserRecord call() throws Exception {
userManager.updateUser(request);
userManager.updateUser(request, jsonFactory);
return userManager.getUserById(request.getUid());
}
});
Expand All @@ -418,6 +461,35 @@ public ApiFuture<UserRecord> updateUserAsync(final UpdateRequest request) {
return new TaskToApiFuture<>(updateUser(request));
}

private Task<Void> setCustomClaims(String uid, Map<String, Object> claims) {
checkNotDestroyed();
final UpdateRequest request = new UpdateRequest(uid).setCustomClaims(claims);
return call(new Callable<Void>() {
@Override
public Void call() throws Exception {
userManager.updateUser(request, jsonFactory);
return null;
}
});
}

/**
* Sets the specified custom claims on an existing user account. A null claims value removes
* any claims currently set on the user account. The claims should serialize into a valid JSON
* string. The serialized claims must not be larger than 1000 characters.
*
* @param uid A user ID string.
* @param claims A map of custom claims or null.
* @return An {@code ApiFuture} which will complete successfully when the user account has been
* updated. If an error occurs while deleting the user account, the future throws a
* {@link FirebaseAuthException}.
* @throws IllegalArgumentException If the user ID string is null or empty, or the claims
* payload is invalid or too large.
*/
public ApiFuture<Void> setCustomClaimsAsync(String uid, Map<String, Object> claims) {
return new TaskToApiFuture<>(setCustomClaims(uid, claims));
}

/**
* Similar to {@link #deleteUserAsync(String)}, but returns a {@link Task}.
*
Expand All @@ -440,19 +512,6 @@ public Void call() throws Exception {
});
}

private void checkNotDestroyed() {
synchronized (lock) {
checkState(!destroyed.get(), "FirebaseAuth instance is no longer alive. This happens when "
+ "the parent FirebaseApp instance has been deleted.");
}
}

private void destroy() {
synchronized (lock) {
destroyed.set(true);
}
}

/**
* Deletes the user identified by the specified user ID.
*
Expand All @@ -470,6 +529,19 @@ private <T> Task<T> call(Callable<T> command) {
return ImplFirebaseTrampolines.submitCallable(firebaseApp, command);
}

private void checkNotDestroyed() {
synchronized (lock) {
checkState(!destroyed.get(), "FirebaseAuth instance is no longer alive. This happens when "
+ "the parent FirebaseApp instance has been deleted.");
}
}

private void destroy() {
synchronized (lock) {
destroyed.set(true);
}
}

private static final String SERVICE_ID = FirebaseAuth.class.getName();

private static class FirebaseAuthService extends FirebaseService<FirebaseAuth> {
Expand Down
35 changes: 33 additions & 2 deletions src/main/java/com/google/firebase/auth/FirebaseUserManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@
import com.google.common.collect.ImmutableMap;
import com.google.firebase.auth.UserRecord.CreateRequest;
import com.google.firebase.auth.UserRecord.UpdateRequest;
import com.google.firebase.auth.internal.DownloadAccountResponse;
import com.google.firebase.auth.internal.GetAccountInfoResponse;

import com.google.firebase.internal.SdkUtils;
import java.io.IOException;
import java.util.List;
import java.util.Map;

/**
Expand All @@ -56,8 +58,15 @@ class FirebaseUserManager {
static final String USER_CREATE_ERROR = "USER_CREATE_ERROR";
static final String USER_UPDATE_ERROR = "USER_UPDATE_ERROR";
static final String USER_DELETE_ERROR = "USER_DELETE_ERROR";
static final String LIST_USERS_ERROR = "LIST_USERS_ERROR";
static final String INTERNAL_ERROR = "INTERNAL_ERROR";

static final int MAX_LIST_USERS_RESULTS = 1000;

static final List<String> RESERVED_CLAIMS = ImmutableList.of(
"amr", "at_hash", "aud", "auth_time", "azp", "cnf", "c_hash", "exp", "iat",
"iss", "jti", "nbf", "nonce", "sub", "firebase");

private static final String ID_TOOLKIT_URL =
"https://www.googleapis.com/identitytoolkit/v3/relyingparty/";
private static final String CLIENT_VERSION_HEADER = "X-Client-Version";
Expand Down Expand Up @@ -157,10 +166,10 @@ String createUser(CreateRequest request) throws FirebaseAuthException {
throw new FirebaseAuthException(USER_CREATE_ERROR, "Failed to create new user");
}

void updateUser(UpdateRequest request) throws FirebaseAuthException {
void updateUser(UpdateRequest request, JsonFactory jsonFactory) throws FirebaseAuthException {
GenericJson response;
try {
response = post("setAccountInfo", request.getProperties(), GenericJson.class);
response = post("setAccountInfo", request.getProperties(jsonFactory), GenericJson.class);
} catch (IOException e) {
throw new FirebaseAuthException(USER_UPDATE_ERROR,
"IO error while updating user: " + request.getUid(), e);
Expand All @@ -187,6 +196,28 @@ void deleteUser(String uid) throws FirebaseAuthException {
}
}

DownloadAccountResponse listUsers(int maxResults, String pageToken) throws FirebaseAuthException {
ImmutableMap.Builder<String, Object> builder = ImmutableMap.<String, Object>builder()
.put("maxResults", maxResults);
if (pageToken != null) {
checkArgument(!pageToken.equals(ListUsersPage.END_OF_LIST), "invalid end of list page token");
builder.put("nextPageToken", pageToken);
}

DownloadAccountResponse response;
try {
response = post("downloadAccount", builder.build(), DownloadAccountResponse.class);
if (response == null) {
throw new FirebaseAuthException(LIST_USERS_ERROR,
"Unexpected response from download user account API.");
}
return response;
} catch (IOException e) {
throw new FirebaseAuthException(LIST_USERS_ERROR,
"IO error while downloading user accounts.", e);
}
}

private <T> T post(String path, Object content, Class<T> clazz) throws IOException {
checkArgument(!Strings.isNullOrEmpty(path), "path must not be null or empty");
checkNotNull(content, "content must not be null");
Expand Down
Loading