|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 package org.mozilla.gecko.background.fxa; |
|
6 |
|
7 import java.net.URI; |
|
8 import java.util.concurrent.Executor; |
|
9 |
|
10 import org.json.simple.JSONObject; |
|
11 import org.mozilla.gecko.background.common.log.Logger; |
|
12 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException; |
|
13 import org.mozilla.gecko.fxa.FxAccountConstants; |
|
14 import org.mozilla.gecko.sync.ExtendedJSONObject; |
|
15 import org.mozilla.gecko.sync.Utils; |
|
16 import org.mozilla.gecko.sync.net.BaseResource; |
|
17 |
|
18 import ch.boye.httpclientandroidlib.HttpResponse; |
|
19 |
|
20 public class FxAccountClient20 extends FxAccountClient10 implements FxAccountClient { |
|
21 protected static final String[] LOGIN_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_UID, JSON_KEY_SESSIONTOKEN }; |
|
22 protected static final String[] LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS = new String[] { JSON_KEY_UID, JSON_KEY_SESSIONTOKEN, JSON_KEY_KEYFETCHTOKEN, }; |
|
23 protected static final String[] LOGIN_RESPONSE_REQUIRED_BOOLEAN_FIELDS = new String[] { JSON_KEY_VERIFIED }; |
|
24 |
|
25 public FxAccountClient20(String serverURI, Executor executor) { |
|
26 super(serverURI, executor); |
|
27 } |
|
28 |
|
29 /** |
|
30 * Thin container for login response. |
|
31 * <p> |
|
32 * The <code>remoteEmail</code> field is the email address as normalized by the |
|
33 * server, and is <b>not necessarily</b> the email address delivered to the |
|
34 * <code>login</code> or <code>create</code> call. |
|
35 */ |
|
36 public static class LoginResponse { |
|
37 public final String remoteEmail; |
|
38 public final String uid; |
|
39 public final byte[] sessionToken; |
|
40 public final boolean verified; |
|
41 public final byte[] keyFetchToken; |
|
42 |
|
43 public LoginResponse(String remoteEmail, String uid, boolean verified, byte[] sessionToken, byte[] keyFetchToken) { |
|
44 this.remoteEmail = remoteEmail; |
|
45 this.uid = uid; |
|
46 this.verified = verified; |
|
47 this.sessionToken = sessionToken; |
|
48 this.keyFetchToken = keyFetchToken; |
|
49 } |
|
50 } |
|
51 |
|
52 // Public for testing only; prefer login and loginAndGetKeys (without boolean parameter). |
|
53 public void login(final byte[] emailUTF8, final byte[] quickStretchedPW, final boolean getKeys, |
|
54 final RequestDelegate<LoginResponse> delegate) { |
|
55 BaseResource resource; |
|
56 JSONObject body; |
|
57 final String path = getKeys ? "account/login?keys=true" : "account/login"; |
|
58 try { |
|
59 resource = new BaseResource(new URI(serverURI + path)); |
|
60 body = new FxAccount20LoginDelegate(emailUTF8, quickStretchedPW).getCreateBody(); |
|
61 } catch (Exception e) { |
|
62 invokeHandleError(delegate, e); |
|
63 return; |
|
64 } |
|
65 |
|
66 resource.delegate = new ResourceDelegate<LoginResponse>(resource, delegate) { |
|
67 @Override |
|
68 public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { |
|
69 try { |
|
70 final String[] requiredStringFields = getKeys ? LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS : LOGIN_RESPONSE_REQUIRED_STRING_FIELDS; |
|
71 body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class); |
|
72 |
|
73 final String[] requiredBooleanFields = LOGIN_RESPONSE_REQUIRED_BOOLEAN_FIELDS; |
|
74 body.throwIfFieldsMissingOrMisTyped(requiredBooleanFields, Boolean.class); |
|
75 |
|
76 String uid = body.getString(JSON_KEY_UID); |
|
77 boolean verified = body.getBoolean(JSON_KEY_VERIFIED); |
|
78 byte[] sessionToken = Utils.hex2Byte(body.getString(JSON_KEY_SESSIONTOKEN)); |
|
79 byte[] keyFetchToken = null; |
|
80 if (getKeys) { |
|
81 keyFetchToken = Utils.hex2Byte(body.getString(JSON_KEY_KEYFETCHTOKEN)); |
|
82 } |
|
83 LoginResponse loginResponse = new LoginResponse(new String(emailUTF8, "UTF-8"), uid, verified, sessionToken, keyFetchToken); |
|
84 |
|
85 delegate.handleSuccess(loginResponse); |
|
86 return; |
|
87 } catch (Exception e) { |
|
88 delegate.handleError(e); |
|
89 return; |
|
90 } |
|
91 } |
|
92 }; |
|
93 |
|
94 post(resource, body, delegate); |
|
95 } |
|
96 |
|
97 public void createAccount(final byte[] emailUTF8, final byte[] quickStretchedPW, final boolean getKeys, final boolean preVerified, |
|
98 final RequestDelegate<LoginResponse> delegate) { |
|
99 BaseResource resource; |
|
100 JSONObject body; |
|
101 final String path = getKeys ? "account/create?keys=true" : "account/create"; |
|
102 try { |
|
103 resource = new BaseResource(new URI(serverURI + path)); |
|
104 body = new FxAccount20CreateDelegate(emailUTF8, quickStretchedPW, preVerified).getCreateBody(); |
|
105 } catch (Exception e) { |
|
106 invokeHandleError(delegate, e); |
|
107 return; |
|
108 } |
|
109 |
|
110 // This is very similar to login, except verified is not required. |
|
111 resource.delegate = new ResourceDelegate<LoginResponse>(resource, delegate) { |
|
112 @Override |
|
113 public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { |
|
114 try { |
|
115 final String[] requiredStringFields = getKeys ? LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS : LOGIN_RESPONSE_REQUIRED_STRING_FIELDS; |
|
116 body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class); |
|
117 |
|
118 String uid = body.getString(JSON_KEY_UID); |
|
119 boolean verified = false; // In production, we're definitely not verified immediately upon creation. |
|
120 Boolean tempVerified = body.getBoolean(JSON_KEY_VERIFIED); |
|
121 if (tempVerified != null) { |
|
122 verified = tempVerified.booleanValue(); |
|
123 } |
|
124 byte[] sessionToken = Utils.hex2Byte(body.getString(JSON_KEY_SESSIONTOKEN)); |
|
125 byte[] keyFetchToken = null; |
|
126 if (getKeys) { |
|
127 keyFetchToken = Utils.hex2Byte(body.getString(JSON_KEY_KEYFETCHTOKEN)); |
|
128 } |
|
129 LoginResponse loginResponse = new LoginResponse(new String(emailUTF8, "UTF-8"), uid, verified, sessionToken, keyFetchToken); |
|
130 |
|
131 delegate.handleSuccess(loginResponse); |
|
132 return; |
|
133 } catch (Exception e) { |
|
134 delegate.handleError(e); |
|
135 return; |
|
136 } |
|
137 } |
|
138 }; |
|
139 |
|
140 post(resource, body, delegate); |
|
141 } |
|
142 |
|
143 @Override |
|
144 public void createAccountAndGetKeys(byte[] emailUTF8, PasswordStretcher passwordStretcher, RequestDelegate<LoginResponse> delegate) { |
|
145 try { |
|
146 byte[] quickStretchedPW = passwordStretcher.getQuickStretchedPW(emailUTF8); |
|
147 createAccount(emailUTF8, quickStretchedPW, true, false, delegate); |
|
148 } catch (Exception e) { |
|
149 invokeHandleError(delegate, e); |
|
150 return; |
|
151 } |
|
152 } |
|
153 |
|
154 @Override |
|
155 public void loginAndGetKeys(byte[] emailUTF8, PasswordStretcher passwordStretcher, RequestDelegate<LoginResponse> delegate) { |
|
156 login(emailUTF8, passwordStretcher, true, delegate); |
|
157 } |
|
158 |
|
159 /** |
|
160 * We want users to be able to enter their email address case-insensitively. |
|
161 * We stretch the password locally using the email address as a salt, to make |
|
162 * dictionary attacks more expensive. This means that a client with a |
|
163 * case-differing email address is unable to produce the correct |
|
164 * authorization, even though it knows the password. In this case, the server |
|
165 * returns the email that the account was created with, so that the client can |
|
166 * re-stretch the password locally with the correct email salt. This version |
|
167 * of <code>login</code> retries at most one time with a server provided email |
|
168 * address. |
|
169 * <p> |
|
170 * Be aware that consumers will not see the initial error response from the |
|
171 * server providing an alternate email (if there is one). |
|
172 * |
|
173 * @param emailUTF8 |
|
174 * user entered email address. |
|
175 * @param stretcher |
|
176 * delegate to stretch and re-stretch password. |
|
177 * @param getKeys |
|
178 * true if a <code>keyFetchToken</code> should be returned (in |
|
179 * addition to the standard <code>sessionToken</code>). |
|
180 * @param delegate |
|
181 * to invoke callbacks. |
|
182 */ |
|
183 public void login(final byte[] emailUTF8, final PasswordStretcher stretcher, final boolean getKeys, |
|
184 final RequestDelegate<LoginResponse> delegate) { |
|
185 byte[] quickStretchedPW; |
|
186 try { |
|
187 FxAccountConstants.pii(LOG_TAG, "Trying user provided email: '" + new String(emailUTF8, "UTF-8") + "'" ); |
|
188 quickStretchedPW = stretcher.getQuickStretchedPW(emailUTF8); |
|
189 } catch (Exception e) { |
|
190 delegate.handleError(e); |
|
191 return; |
|
192 } |
|
193 |
|
194 this.login(emailUTF8, quickStretchedPW, getKeys, new RequestDelegate<LoginResponse>() { |
|
195 @Override |
|
196 public void handleSuccess(LoginResponse result) { |
|
197 delegate.handleSuccess(result); |
|
198 } |
|
199 |
|
200 @Override |
|
201 public void handleError(Exception e) { |
|
202 delegate.handleError(e); |
|
203 } |
|
204 |
|
205 @Override |
|
206 public void handleFailure(FxAccountClientRemoteException e) { |
|
207 String alternateEmail = e.body.getString(JSON_KEY_EMAIL); |
|
208 if (!e.isBadEmailCase() || alternateEmail == null) { |
|
209 delegate.handleFailure(e); |
|
210 return; |
|
211 }; |
|
212 |
|
213 Logger.info(LOG_TAG, "Server returned alternate email; retrying login with provided email."); |
|
214 FxAccountConstants.pii(LOG_TAG, "Trying server provided email: '" + alternateEmail + "'" ); |
|
215 |
|
216 try { |
|
217 // Nota bene: this is not recursive, since we call the fixed password |
|
218 // signature here, which invokes a non-retrying version. |
|
219 byte[] alternateEmailUTF8 = alternateEmail.getBytes("UTF-8"); |
|
220 byte[] alternateQuickStretchedPW = stretcher.getQuickStretchedPW(alternateEmailUTF8); |
|
221 login(alternateEmailUTF8, alternateQuickStretchedPW, getKeys, delegate); |
|
222 } catch (Exception innerException) { |
|
223 delegate.handleError(innerException); |
|
224 return; |
|
225 } |
|
226 } |
|
227 }); |
|
228 } |
|
229 } |