Skip to content

Commit 093c28e

Browse files
authored
Implementing the User Management API (firebase#34)
* Implementing the user management API * Added NewAccount type * Implemented user update functionality; Added more test cases; Implemented User.Builder and User.Updater classes * More test coverage * Exposing user management functionality via FirebaseAuth; More tests * Cleaned up User and Provider APIs by introducing separate classes for JSON data bindings; More tests * Implemented deleteUser method; Removed JsonHttpClient class (too simple to be a class of its own) * Improved error handling * More tests around error handling * Updating API docs * Added more documentation; Renamed Provider to ProviderUserInfo for clarity * Made User timestamp properties into Date types * Made the dependency on google http client explicit * Moved creation time and sign in time to UserMetadata type * Adding newline to end of file * Addressing some minor issues with javadocs and annotations
1 parent 9c87437 commit 093c28e

17 files changed

Lines changed: 1593 additions & 7 deletions

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,11 @@
320320
<artifactId>google-api-client-gson</artifactId>
321321
<version>1.22.0</version>
322322
</dependency>
323+
<dependency>
324+
<groupId>com.google.http-client</groupId>
325+
<artifactId>google-http-client</artifactId>
326+
<version>1.22.0</version>
327+
</dependency>
323328
<dependency>
324329
<groupId>org.json</groupId>
325330
<artifactId>json</artifactId>

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

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,24 @@
1616

1717
package com.google.firebase.auth;
1818

19+
import static com.google.common.base.Preconditions.checkArgument;
20+
import static com.google.common.base.Preconditions.checkNotNull;
21+
1922
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
2023
import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager;
24+
import com.google.api.client.googleapis.util.Utils;
2125
import com.google.api.client.json.JsonFactory;
2226
import com.google.api.client.json.gson.GsonFactory;
2327
import com.google.api.client.util.Clock;
2428
import com.google.common.annotations.VisibleForTesting;
29+
import com.google.common.base.Strings;
2530
import com.google.firebase.FirebaseApp;
2631
import com.google.firebase.FirebaseException;
2732
import com.google.firebase.ImplFirebaseTrampolines;
2833
import com.google.firebase.auth.internal.FirebaseTokenFactory;
2934
import com.google.firebase.auth.internal.FirebaseTokenVerifier;
3035
import com.google.firebase.internal.FirebaseService;
36+
import com.google.firebase.internal.GetTokenResult;
3137
import com.google.firebase.internal.NonNull;
3238
import com.google.firebase.tasks.Continuation;
3339
import com.google.firebase.tasks.Task;
@@ -51,6 +57,7 @@ public class FirebaseAuth {
5157
private final FirebaseApp firebaseApp;
5258
private final GooglePublicKeysManager googlePublicKeysManager;
5359
private final Clock clock;
60+
private final FirebaseUserManager userManager;
5461

5562
private FirebaseAuth(FirebaseApp firebaseApp) {
5663
this(firebaseApp, FirebaseTokenVerifier.DEFAULT_KEY_MANAGER, Clock.SYSTEM);
@@ -66,6 +73,7 @@ private FirebaseAuth(FirebaseApp firebaseApp) {
6673
this.firebaseApp = firebaseApp;
6774
this.googlePublicKeysManager = googlePublicKeysManager;
6875
this.clock = clock;
76+
this.userManager = new FirebaseUserManager(jsonFactory, Utils.getDefaultTransport());
6977
}
7078

7179
/**
@@ -195,6 +203,110 @@ public FirebaseToken then(@NonNull Task<String> task) throws Exception {
195203
});
196204
}
197205

206+
/**
207+
* Gets the user data corresponding to the specified user ID.
208+
*
209+
* @param uid A user ID string.
210+
* @return A {@link Task} which will complete successfully with a {@link User} instance.
211+
* If an error occurs while retrieving user data or if the specified user ID does not exist,
212+
* the task fails with a FirebaseAuthException.
213+
* @throws IllegalArgumentException If the user ID string is null or empty.
214+
*/
215+
public Task<User> getUser(final String uid) {
216+
checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
217+
return ImplFirebaseTrampolines.getToken(firebaseApp, false).continueWith(
218+
new Continuation<GetTokenResult, User>() {
219+
@Override
220+
public User then(Task<GetTokenResult> task) throws Exception {
221+
return userManager.getUserById(uid, task.getResult().getToken());
222+
}
223+
});
224+
}
225+
226+
/**
227+
* Gets the user data corresponding to the specified user email.
228+
*
229+
* @param email A user email address string.
230+
* @return A {@link Task} which will complete successfully with a {@link User} instance.
231+
* If an error occurs while retrieving user data or if the email address does not correspond
232+
* to a user, the task fails with a FirebaseAuthException.
233+
* @throws IllegalArgumentException If the email is null or empty.
234+
*/
235+
public Task<User> getUserByEmail(final String email) {
236+
checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty");
237+
return ImplFirebaseTrampolines.getToken(firebaseApp, false).continueWith(
238+
new Continuation<GetTokenResult, User>() {
239+
@Override
240+
public User then(Task<GetTokenResult> task) throws Exception {
241+
return userManager.getUserByEmail(email, task.getResult().getToken());
242+
}
243+
});
244+
}
245+
246+
/**
247+
* Creates a new user account with the attributes contained in the specified {@link User.Builder}.
248+
*
249+
* @param builder A non-null {@link User.Builder} instance.
250+
* @return A {@link Task} which will complete successfully with a {@link User} instance
251+
* corresponding to the newly created account. If an error occurs while creating the user
252+
* account, the task fails with a FirebaseAuthException.
253+
* @throws NullPointerException if the provided builder is null.
254+
*/
255+
public Task<User> createUser(final User.Builder builder) {
256+
checkNotNull(builder, "builder must not be null");
257+
return ImplFirebaseTrampolines.getToken(firebaseApp, false).continueWith(
258+
new Continuation<GetTokenResult, User>() {
259+
@Override
260+
public User then(Task<GetTokenResult> task) throws Exception {
261+
String uid = userManager.createUser(builder, task.getResult().getToken());
262+
return userManager.getUserById(uid, task.getResult().getToken());
263+
}
264+
});
265+
}
266+
267+
/**
268+
* Updates an existing user account with the attributes contained in the specified
269+
* {@link User.Updater}.
270+
*
271+
* @param updater A non-null {@link User.Updater} instance.
272+
* @return A {@link Task} which will complete successfully with a {@link User} instance
273+
* corresponding to the updated user account. If an error occurs while updating the user
274+
* account, the task fails with a FirebaseAuthException.
275+
* @throws NullPointerException if the provided updater is null.
276+
*/
277+
public Task<User> updateUser(final User.Updater updater) {
278+
checkNotNull(updater, "updater must not be null");
279+
return ImplFirebaseTrampolines.getToken(firebaseApp, false).continueWith(
280+
new Continuation<GetTokenResult, User>() {
281+
@Override
282+
public User then(Task<GetTokenResult> task) throws Exception {
283+
userManager.updateUser(updater, task.getResult().getToken());
284+
return userManager.getUserById(updater.getUid(), task.getResult().getToken());
285+
}
286+
});
287+
}
288+
289+
/**
290+
* Deletes the user identified by the specified user ID.
291+
*
292+
* @param uid A user ID string.
293+
* @return A {@link Task} which will complete successfully when the specified user account has
294+
* been deleted. If an error occurs while deleting the user account, the task fails with a
295+
* FirebaseAuthException.
296+
* @throws IllegalArgumentException If the user ID string is null or empty.
297+
*/
298+
public Task<Void> deleteUser(final String uid) {
299+
checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
300+
return ImplFirebaseTrampolines.getToken(firebaseApp, false).continueWith(
301+
new Continuation<GetTokenResult, Void>() {
302+
@Override
303+
public Void then(Task<GetTokenResult> task) throws Exception {
304+
userManager.deleteUser(uid, task.getResult().getToken());
305+
return null;
306+
}
307+
});
308+
}
309+
198310
private static final String SERVICE_ID = FirebaseAuth.class.getName();
199311

200312
private static class FirebaseAuthService extends FirebaseService<FirebaseAuth> {
@@ -206,8 +318,8 @@ private static class FirebaseAuthService extends FirebaseService<FirebaseAuth> {
206318
@Override
207319
public void destroy() {
208320
// NOTE: We don't explicitly tear down anything here, but public methods of FirebaseAuth
209-
// will now fail because calls to getCredential() will hit FirebaseApp.getOptions() which
210-
// will throw once the app is deleted.
321+
// will now fail because calls to getCredential() and getToken() will hit FirebaseApp,
322+
// which will throw once the app is deleted.
211323
}
212324
}
213325
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@ public class FirebaseAuthException extends FirebaseException {
3737
private final String errorCode;
3838

3939
public FirebaseAuthException(@NonNull String errorCode, @NonNull String detailMessage) {
40-
super(detailMessage);
40+
this(errorCode, detailMessage, null);
41+
}
42+
43+
public FirebaseAuthException(@NonNull String errorCode, @NonNull String detailMessage,
44+
Throwable throwable) {
45+
super(detailMessage, throwable);
4146
checkArgument(!Strings.isNullOrEmpty(errorCode));
4247
this.errorCode = errorCode;
4348
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ public class FirebaseCredentials {
5050
private static final List<String> FIREBASE_SCOPES =
5151
ImmutableList.of(
5252
"https://www.googleapis.com/auth/firebase.database",
53-
"https://www.googleapis.com/auth/userinfo.email");
53+
"https://www.googleapis.com/auth/userinfo.email",
54+
"https://www.googleapis.com/auth/identitytoolkit");
5455

5556
private FirebaseCredentials() {
5657
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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+
import static com.google.common.base.Preconditions.checkNotNull;
21+
22+
import com.google.api.client.http.GenericUrl;
23+
import com.google.api.client.http.HttpRequest;
24+
import com.google.api.client.http.HttpRequestFactory;
25+
import com.google.api.client.http.HttpResponse;
26+
import com.google.api.client.http.HttpTransport;
27+
import com.google.api.client.http.json.JsonHttpContent;
28+
import com.google.api.client.json.GenericJson;
29+
import com.google.api.client.json.JsonFactory;
30+
import com.google.api.client.json.JsonObjectParser;
31+
import com.google.common.base.Strings;
32+
import com.google.common.collect.ImmutableList;
33+
import com.google.common.collect.ImmutableMap;
34+
import com.google.firebase.auth.internal.GetAccountInfoResponse;
35+
36+
import java.io.IOException;
37+
import java.util.Map;
38+
39+
/**
40+
* FirebaseUserManager provides methods for interacting with the Google Identity Toolkit via its
41+
* REST API. This class does not hold any mutable state, and is thread safe.
42+
*
43+
* @see <a href="https://developers.google.com/identity/toolkit/web/reference/relyingparty">
44+
* Google Identity Toolkit</a>
45+
*/
46+
class FirebaseUserManager {
47+
48+
static final String USER_NOT_FOUND_ERROR = "USER_NOT_FOUND_ERROR";
49+
static final String USER_CREATE_ERROR = "USER_CREATE_ERROR";
50+
static final String USER_UPDATE_ERROR = "USER_UPDATE_ERROR";
51+
static final String USER_DELETE_ERROR = "USER_DELETE_ERROR";
52+
static final String INTERNAL_ERROR = "INTERNAL_ERROR";
53+
54+
private static final String ID_TOOLKIT_URL =
55+
"https://www.googleapis.com/identitytoolkit/v3/relyingparty/";
56+
57+
private final JsonFactory jsonFactory;
58+
private final HttpRequestFactory requestFactory;
59+
60+
/**
61+
* Creates a new FirebaseUserManager instance.
62+
*
63+
* @param jsonFactory JsonFactory instance used to transform Java objects into JSON and back.
64+
* @param transport HttpTransport used to make REST API calls.
65+
*/
66+
FirebaseUserManager(JsonFactory jsonFactory, HttpTransport transport) {
67+
this.jsonFactory = checkNotNull(jsonFactory, "jsonFactory must not be null");
68+
this.requestFactory = transport.createRequestFactory();
69+
}
70+
71+
User getUserById(String uid, String token) throws FirebaseAuthException {
72+
final Map<String, Object> payload = ImmutableMap.<String, Object>of(
73+
"localId", ImmutableList.of(uid));
74+
GetAccountInfoResponse response;
75+
try {
76+
response = post("getAccountInfo", token, payload,
77+
GetAccountInfoResponse.class);
78+
} catch (IOException e) {
79+
throw new FirebaseAuthException(INTERNAL_ERROR,
80+
"IO error while retrieving user with ID: " + uid, e);
81+
}
82+
83+
if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) {
84+
throw new FirebaseAuthException(USER_NOT_FOUND_ERROR,
85+
"No user record found for the provided user ID: " + uid);
86+
}
87+
return new User(response.getUsers().get(0));
88+
}
89+
90+
User getUserByEmail(String email, String token) throws FirebaseAuthException {
91+
final Map<String, Object> payload = ImmutableMap.<String, Object>of(
92+
"email", ImmutableList.of(email));
93+
GetAccountInfoResponse response;
94+
try {
95+
response = post("getAccountInfo", token, payload, GetAccountInfoResponse.class);
96+
} catch (IOException e) {
97+
throw new FirebaseAuthException(INTERNAL_ERROR,
98+
"IO error while retrieving user with email: " + email, e);
99+
}
100+
101+
if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) {
102+
throw new FirebaseAuthException(USER_NOT_FOUND_ERROR,
103+
"No user record found for the provided email: " + email);
104+
}
105+
return new User(response.getUsers().get(0));
106+
}
107+
108+
String createUser(User.Builder builder, String token) throws FirebaseAuthException {
109+
GenericJson response;
110+
try {
111+
response = post("signupNewUser", token, builder.build(), GenericJson.class);
112+
} catch (IOException e) {
113+
throw new FirebaseAuthException(USER_CREATE_ERROR,
114+
"IO error while creating user account", e);
115+
}
116+
117+
if (response != null) {
118+
String uid = (String) response.get("localId");
119+
if (!Strings.isNullOrEmpty(uid)) {
120+
return uid;
121+
}
122+
}
123+
throw new FirebaseAuthException(USER_CREATE_ERROR, "Failed to create new user");
124+
}
125+
126+
void updateUser(User.Updater updater, String token) throws FirebaseAuthException {
127+
GenericJson response;
128+
try {
129+
response = post("setAccountInfo", token, updater.update(), GenericJson.class);
130+
} catch (IOException e) {
131+
throw new FirebaseAuthException(USER_UPDATE_ERROR,
132+
"IO error while updating user: " + updater.getUid(), e);
133+
}
134+
135+
if (response == null || !updater.getUid().equals(response.get("localId"))) {
136+
throw new FirebaseAuthException(USER_UPDATE_ERROR,
137+
"Failed to update user: " + updater.getUid());
138+
}
139+
}
140+
141+
void deleteUser(String uid, String token) throws FirebaseAuthException {
142+
final Map<String, Object> payload = ImmutableMap.<String, Object>of("localId", uid);
143+
GenericJson response;
144+
try {
145+
response = post("deleteAccount", token, payload, GenericJson.class);
146+
} catch (IOException e) {
147+
throw new FirebaseAuthException(USER_DELETE_ERROR,
148+
"IO error while deleting user: " + uid, e);
149+
}
150+
if (response == null || !response.containsKey("kind")) {
151+
throw new FirebaseAuthException(USER_DELETE_ERROR,
152+
"Failed to delete user: " + uid);
153+
}
154+
}
155+
156+
private <T> T post(String path, String token, Object content, Class<T> clazz) throws IOException {
157+
checkArgument(!Strings.isNullOrEmpty(path), "path must not be null or empty");
158+
checkArgument(!Strings.isNullOrEmpty(token), "OAuth token must not be null or empty");
159+
checkNotNull(content, "content must not be null");
160+
checkNotNull(clazz, "response class must not be null");
161+
162+
GenericUrl url = new GenericUrl(ID_TOOLKIT_URL + path);
163+
HttpRequest request = requestFactory.buildPostRequest(url,
164+
new JsonHttpContent(jsonFactory, content));
165+
request.setParser(new JsonObjectParser(jsonFactory));
166+
request.getHeaders().setAuthorization("Bearer " + token);
167+
HttpResponse response = request.execute();
168+
try {
169+
return response.parseAs(clazz);
170+
} finally {
171+
response.disconnect();
172+
}
173+
}
174+
}

0 commit comments

Comments
 (0)