Skip to content

Commit 2a6f45f

Browse files
authored
fix(auth): Migrated user management APIs to the new error handling scheme (#360)
* Error handling revamp in FirebaseUserManager * Updated integration tests; Added documentation * Assigning the correct ErrorCode for auth errors * Moved AuthErrorHandler to a separate top-level class
1 parent c104db6 commit 2a6f45f

File tree

11 files changed

+519
-247
lines changed

11 files changed

+519
-247
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2020 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+
/**
20+
* Error codes that can be raised by the Firebase Auth APIs.
21+
*/
22+
public enum AuthErrorCode {
23+
24+
/**
25+
* A user already exists with the provided email.
26+
*/
27+
EMAIL_ALREADY_EXISTS,
28+
29+
/**
30+
* The provided dynamic link domain is not configured or authorized for the current project.
31+
*/
32+
INVALID_DYNAMIC_LINK_DOMAIN,
33+
34+
/**
35+
* A user already exists with the provided phone number.
36+
*/
37+
PHONE_NUMBER_ALREADY_EXISTS,
38+
39+
/**
40+
* A user already exists with the provided UID.
41+
*/
42+
UID_ALREADY_EXISTS,
43+
44+
/**
45+
* The domain of the continue URL is not whitelisted. Whitelist the domain in the Firebase
46+
* console.
47+
*/
48+
UNAUTHORIZED_CONTINUE_URL,
49+
50+
/**
51+
* No user record found for the given identifier.
52+
*/
53+
USER_NOT_FOUND,
54+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/*
2+
* Copyright 2020 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.checkNotNull;
20+
21+
import com.google.api.client.json.GenericJson;
22+
import com.google.api.client.json.JsonFactory;
23+
import com.google.api.client.util.Key;
24+
import com.google.common.base.Strings;
25+
import com.google.common.collect.ImmutableMap;
26+
import com.google.firebase.ErrorCode;
27+
import com.google.firebase.FirebaseException;
28+
import com.google.firebase.internal.AbstractHttpErrorHandler;
29+
import com.google.firebase.internal.Nullable;
30+
import java.io.IOException;
31+
import java.util.Map;
32+
33+
final class AuthErrorHandler extends AbstractHttpErrorHandler<FirebaseAuthException> {
34+
35+
private static final Map<String, AuthError> ERROR_CODES =
36+
ImmutableMap.<String, AuthError>builder()
37+
.put(
38+
"DUPLICATE_EMAIL",
39+
new AuthError(
40+
ErrorCode.ALREADY_EXISTS,
41+
"The user with the provided email already exists",
42+
AuthErrorCode.EMAIL_ALREADY_EXISTS))
43+
.put(
44+
"DUPLICATE_LOCAL_ID",
45+
new AuthError(
46+
ErrorCode.ALREADY_EXISTS,
47+
"The user with the provided uid already exists",
48+
AuthErrorCode.UID_ALREADY_EXISTS))
49+
.put(
50+
"EMAIL_EXISTS",
51+
new AuthError(
52+
ErrorCode.ALREADY_EXISTS,
53+
"The user with the provided email already exists",
54+
AuthErrorCode.EMAIL_ALREADY_EXISTS))
55+
.put(
56+
"INVALID_DYNAMIC_LINK_DOMAIN",
57+
new AuthError(
58+
ErrorCode.INVALID_ARGUMENT,
59+
"The provided dynamic link domain is not "
60+
+ "configured or authorized for the current project",
61+
AuthErrorCode.INVALID_DYNAMIC_LINK_DOMAIN))
62+
.put(
63+
"PHONE_NUMBER_EXISTS",
64+
new AuthError(
65+
ErrorCode.ALREADY_EXISTS,
66+
"The user with the provided phone number already exists",
67+
AuthErrorCode.PHONE_NUMBER_ALREADY_EXISTS))
68+
.put(
69+
"UNAUTHORIZED_DOMAIN",
70+
new AuthError(
71+
ErrorCode.INVALID_ARGUMENT,
72+
"The domain of the continue URL is not whitelisted",
73+
AuthErrorCode.UNAUTHORIZED_CONTINUE_URL))
74+
.put(
75+
"USER_NOT_FOUND",
76+
new AuthError(
77+
ErrorCode.NOT_FOUND,
78+
"No user record found for the given identifier",
79+
AuthErrorCode.USER_NOT_FOUND))
80+
.build();
81+
82+
private final JsonFactory jsonFactory;
83+
84+
AuthErrorHandler(JsonFactory jsonFactory) {
85+
this.jsonFactory = checkNotNull(jsonFactory);
86+
}
87+
88+
@Override
89+
protected FirebaseAuthException createException(FirebaseException base) {
90+
String response = getResponse(base);
91+
AuthServiceErrorResponse parsed = safeParse(response);
92+
AuthError errorInfo = ERROR_CODES.get(parsed.getCode());
93+
if (errorInfo != null) {
94+
return new FirebaseAuthException(
95+
errorInfo.getErrorCode(),
96+
errorInfo.buildMessage(parsed),
97+
base.getCause(),
98+
base.getHttpResponse(),
99+
errorInfo.getAuthErrorCode());
100+
}
101+
102+
return new FirebaseAuthException(
103+
base.getErrorCodeNew(),
104+
base.getMessage(),
105+
base.getCause(),
106+
base.getHttpResponse(),
107+
null);
108+
}
109+
110+
private String getResponse(FirebaseException base) {
111+
if (base.getHttpResponse() == null) {
112+
return null;
113+
}
114+
115+
return base.getHttpResponse().getContent();
116+
}
117+
118+
private AuthServiceErrorResponse safeParse(String response) {
119+
AuthServiceErrorResponse parsed = new AuthServiceErrorResponse();
120+
if (!Strings.isNullOrEmpty(response)) {
121+
try {
122+
jsonFactory.createJsonParser(response).parse(parsed);
123+
} catch (IOException ignore) {
124+
// Ignore any error that may occur while parsing the error response. The server
125+
// may have responded with a non-json payload.
126+
}
127+
}
128+
129+
return parsed;
130+
}
131+
132+
private static class AuthError {
133+
134+
private final ErrorCode errorCode;
135+
private final String message;
136+
private final AuthErrorCode authErrorCode;
137+
138+
AuthError(ErrorCode errorCode, String message, AuthErrorCode authErrorCode) {
139+
this.errorCode = errorCode;
140+
this.message = message;
141+
this.authErrorCode = authErrorCode;
142+
}
143+
144+
ErrorCode getErrorCode() {
145+
return errorCode;
146+
}
147+
148+
AuthErrorCode getAuthErrorCode() {
149+
return authErrorCode;
150+
}
151+
152+
String buildMessage(AuthServiceErrorResponse response) {
153+
StringBuilder builder = new StringBuilder(this.message)
154+
.append(" (").append(response.getCode()).append(")");
155+
String detail = response.getDetail();
156+
if (!Strings.isNullOrEmpty(detail)) {
157+
builder.append(": ").append(detail);
158+
} else {
159+
builder.append(".");
160+
}
161+
162+
return builder.toString();
163+
}
164+
}
165+
166+
/**
167+
* JSON data binding for JSON error messages sent by Google identity toolkit service. These
168+
* error messages take the form `{"error": {"message": "CODE: OPTIONAL DETAILS"}}`.
169+
*/
170+
private static class AuthServiceErrorResponse {
171+
172+
@Key("error")
173+
private GenericJson error;
174+
175+
@Nullable
176+
public String getCode() {
177+
String message = getMessage();
178+
if (Strings.isNullOrEmpty(message)) {
179+
return null;
180+
}
181+
182+
int separator = message.indexOf(':');
183+
if (separator != -1) {
184+
return message.substring(0, separator);
185+
}
186+
187+
return message;
188+
}
189+
190+
@Nullable
191+
public String getDetail() {
192+
String message = getMessage();
193+
if (Strings.isNullOrEmpty(message)) {
194+
return null;
195+
}
196+
197+
int separator = message.indexOf(':');
198+
if (separator != -1) {
199+
return message.substring(separator + 1).trim();
200+
}
201+
202+
return null;
203+
}
204+
205+
private String getMessage() {
206+
if (error == null) {
207+
return null;
208+
}
209+
210+
return (String) error.get("message");
211+
}
212+
}
213+
}

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

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,40 +16,57 @@
1616

1717
package com.google.firebase.auth;
1818

19-
// TODO: Move it out from firebase-common. Temporary host it their for
20-
// database's integration.http://b/27624510.
21-
22-
// TODO: Decide if changing this not enforcing an error code. Need to align
23-
// with the decision in http://b/27677218. Also, need to turn this into abstract later.
24-
2519
import static com.google.common.base.Preconditions.checkArgument;
2620

2721
import com.google.common.base.Strings;
22+
import com.google.firebase.ErrorCode;
2823
import com.google.firebase.FirebaseException;
24+
import com.google.firebase.IncomingHttpResponse;
2925
import com.google.firebase.internal.NonNull;
26+
import com.google.firebase.internal.Nullable;
3027

3128
/**
3229
* Generic exception related to Firebase Authentication. Check the error code and message for more
3330
* details.
3431
*/
3532
public class FirebaseAuthException extends FirebaseException {
3633

37-
private final String errorCode;
34+
private final AuthErrorCode errorCode;
35+
private final String deprecatedErrorCode;
36+
37+
FirebaseAuthException(
38+
@NonNull ErrorCode errorCode,
39+
@NonNull String message,
40+
Throwable cause,
41+
IncomingHttpResponse response,
42+
AuthErrorCode authErrorCode) {
43+
super(errorCode, message, cause, response);
44+
this.errorCode = authErrorCode;
45+
this.deprecatedErrorCode = null;
46+
}
3847

48+
@Deprecated
3949
public FirebaseAuthException(@NonNull String errorCode, @NonNull String detailMessage) {
4050
this(errorCode, detailMessage, null);
4151
}
4252

43-
public FirebaseAuthException(@NonNull String errorCode, @NonNull String detailMessage,
44-
Throwable throwable) {
53+
@Deprecated
54+
public FirebaseAuthException(
55+
@NonNull String errorCode, @NonNull String detailMessage, Throwable throwable) {
4556
super(detailMessage, throwable);
4657
checkArgument(!Strings.isNullOrEmpty(errorCode));
47-
this.errorCode = errorCode;
58+
this.errorCode = null;
59+
this.deprecatedErrorCode = errorCode;
4860
}
4961

50-
/** Returns an error code that may provide more information about the error. */
51-
@NonNull
52-
public String getErrorCode() {
62+
@Nullable
63+
public AuthErrorCode getAuthErrorCode() {
5364
return errorCode;
5465
}
66+
67+
/** Returns an error code that may provide more information about the error. */
68+
@Deprecated
69+
public String getDeprecatedErrorCode() {
70+
return deprecatedErrorCode;
71+
}
5572
}

0 commit comments

Comments
 (0)