1 // Copyright (c) 2009 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/common/net/gaia/gaia_authenticator.h"
6
7 #include <string>
8 #include <utility>
9 #include <vector>
10
11 #include "base/basictypes.h"
12 #include "base/port.h"
13 #include "base/string_split.h"
14 #include "chrome/common/deprecated/event_sys-inl.h"
15 #include "chrome/common/net/http_return.h"
16 #include "googleurl/src/gurl.h"
17 #include "net/base/escape.h"
18
19 using std::pair;
20 using std::string;
21 using std::vector;
22
23 namespace gaia {
24
25 static const char kGaiaV1IssueAuthTokenPath[] = "/accounts/IssueAuthToken";
26
27 static const char kGetUserInfoPath[] = "/accounts/GetUserInfo";
28
AuthResults()29 GaiaAuthenticator::AuthResults::AuthResults() : auth_error(None) {}
30
~AuthResults()31 GaiaAuthenticator::AuthResults::~AuthResults() {}
32
AuthParams()33 GaiaAuthenticator::AuthParams::AuthParams() : authenticator(NULL),
34 request_id(0) {}
35
~AuthParams()36 GaiaAuthenticator::AuthParams::~AuthParams() {}
37
38 // Sole constructor with initializers for all fields.
GaiaAuthenticator(const string & user_agent,const string & service_id,const string & gaia_url)39 GaiaAuthenticator::GaiaAuthenticator(const string& user_agent,
40 const string& service_id,
41 const string& gaia_url)
42 : user_agent_(user_agent),
43 service_id_(service_id),
44 gaia_url_(gaia_url),
45 request_count_(0),
46 delay_(0),
47 next_allowed_auth_attempt_time_(0),
48 early_auth_attempt_count_(0),
49 message_loop_(NULL) {
50 GaiaAuthEvent done = { GaiaAuthEvent::GAIA_AUTHENTICATOR_DESTROYED, None,
51 this };
52 channel_ = new Channel(done);
53 }
54
~GaiaAuthenticator()55 GaiaAuthenticator::~GaiaAuthenticator() {
56 delete channel_;
57 }
58
59 // mutex_ must be entered before calling this function.
MakeParams(const string & user_name,const string & password,const string & captcha_token,const string & captcha_value)60 GaiaAuthenticator::AuthParams GaiaAuthenticator::MakeParams(
61 const string& user_name,
62 const string& password,
63 const string& captcha_token,
64 const string& captcha_value) {
65 AuthParams params;
66 params.request_id = ++request_count_;
67 params.email = user_name;
68 params.password = password;
69 params.captcha_token = captcha_token;
70 params.captcha_value = captcha_value;
71 params.authenticator = this;
72 return params;
73 }
74
Authenticate(const string & user_name,const string & password,const string & captcha_token,const string & captcha_value)75 bool GaiaAuthenticator::Authenticate(const string& user_name,
76 const string& password,
77 const string& captcha_token,
78 const string& captcha_value) {
79 DCHECK_EQ(MessageLoop::current(), message_loop_);
80
81 AuthParams const params =
82 MakeParams(user_name, password, captcha_token, captcha_value);
83 return AuthenticateImpl(params);
84 }
85
AuthenticateWithLsid(const string & lsid)86 bool GaiaAuthenticator::AuthenticateWithLsid(const string& lsid) {
87 auth_results_.lsid = lsid;
88 // We need to lookup the email associated with this LSID cookie in order to
89 // update |auth_results_| with the correct values.
90 if (LookupEmail(&auth_results_)) {
91 auth_results_.email = auth_results_.primary_email;
92 return IssueAuthToken(&auth_results_, service_id_);
93 }
94 return false;
95 }
96
AuthenticateImpl(const AuthParams & params)97 bool GaiaAuthenticator::AuthenticateImpl(const AuthParams& params) {
98 DCHECK_EQ(MessageLoop::current(), message_loop_);
99 AuthResults results;
100 const bool succeeded = AuthenticateImpl(params, &results);
101 if (params.request_id == request_count_) {
102 auth_results_ = results;
103 GaiaAuthEvent event = { succeeded ? GaiaAuthEvent::GAIA_AUTH_SUCCEEDED
104 : GaiaAuthEvent::GAIA_AUTH_FAILED,
105 results.auth_error, this };
106 channel_->NotifyListeners(event);
107 }
108 return succeeded;
109 }
110
111 // This method makes an HTTP request to the Gaia server, and calls other
112 // methods to help parse the response. If authentication succeeded, then
113 // Gaia-issued cookies are available in the respective variables; if
114 // authentication failed, then the exact error is available as an enum. If the
115 // client wishes to save the credentials, the last parameter must be true.
116 // If a subsequent request is made with fresh credentials, the saved credentials
117 // are wiped out; any subsequent request to the zero-parameter overload of this
118 // method preserves the saved credentials.
AuthenticateImpl(const AuthParams & params,AuthResults * results)119 bool GaiaAuthenticator::AuthenticateImpl(const AuthParams& params,
120 AuthResults* results) {
121 DCHECK_EQ(MessageLoop::current(), message_loop_);
122 results->auth_error = ConnectionUnavailable;
123 results->email = params.email.data();
124 results->password = params.password;
125
126 // The aim of this code is to start failing requests if due to a logic error
127 // in the program we're hammering GAIA.
128 #if defined(OS_WIN)
129 __time32_t now = _time32(0);
130 #else // defined(OS_WIN)
131 time_t now = time(0);
132 #endif // defined(OS_WIN)
133
134 if (now > next_allowed_auth_attempt_time_) {
135 next_allowed_auth_attempt_time_ = now + 1;
136 // If we're more than 2 minutes past the allowed time we reset the early
137 // attempt count.
138 if (now - next_allowed_auth_attempt_time_ > 2 * 60) {
139 delay_ = 1;
140 early_auth_attempt_count_ = 0;
141 }
142 } else {
143 ++early_auth_attempt_count_;
144 // Allow 3 attempts, but then limit.
145 if (early_auth_attempt_count_ > 3) {
146 delay_ = GetBackoffDelaySeconds(delay_);
147 next_allowed_auth_attempt_time_ = now + delay_;
148 return false;
149 }
150 }
151
152 return PerformGaiaRequest(params, results);
153 }
154
PerformGaiaRequest(const AuthParams & params,AuthResults * results)155 bool GaiaAuthenticator::PerformGaiaRequest(const AuthParams& params,
156 AuthResults* results) {
157 DCHECK_EQ(MessageLoop::current(), message_loop_);
158 GURL gaia_auth_url(gaia_url_);
159
160 string post_body;
161 post_body += "Email=" + EscapeUrlEncodedData(params.email);
162 post_body += "&Passwd=" + EscapeUrlEncodedData(params.password);
163 post_body += "&source=" + EscapeUrlEncodedData(user_agent_);
164 post_body += "&service=" + service_id_;
165 if (!params.captcha_token.empty() && !params.captcha_value.empty()) {
166 post_body += "&logintoken=" + EscapeUrlEncodedData(params.captcha_token);
167 post_body += "&logincaptcha=" + EscapeUrlEncodedData(params.captcha_value);
168 }
169 post_body += "&PersistentCookie=true";
170 // We set it to GOOGLE (and not HOSTED or HOSTED_OR_GOOGLE) because we only
171 // allow consumer logins.
172 post_body += "&accountType=GOOGLE";
173
174 string message_text;
175 unsigned long server_response_code;
176 if (!Post(gaia_auth_url, post_body, &server_response_code, &message_text)) {
177 results->auth_error = ConnectionUnavailable;
178 return false;
179 }
180
181 // Parse reply in two different ways, depending on if request failed or
182 // succeeded.
183 if (RC_FORBIDDEN == server_response_code) {
184 ExtractAuthErrorFrom(message_text, results);
185 return false;
186 } else if (RC_REQUEST_OK == server_response_code) {
187 ExtractTokensFrom(message_text, results);
188 if (!IssueAuthToken(results, service_id_)) {
189 return false;
190 }
191
192 return LookupEmail(results);
193 } else {
194 results->auth_error = Unknown;
195 return false;
196 }
197 }
198
Post(const GURL & url,const std::string & post_body,unsigned long * response_code,std::string * response_body)199 bool GaiaAuthenticator::Post(const GURL& url,
200 const std::string& post_body,
201 unsigned long* response_code,
202 std::string* response_body) {
203 return false;
204 }
205
LookupEmail(AuthResults * results)206 bool GaiaAuthenticator::LookupEmail(AuthResults* results) {
207 DCHECK_EQ(MessageLoop::current(), message_loop_);
208 // Use the provided Gaia server, but change the path to what V1 expects.
209 GURL url(gaia_url_); // Gaia server.
210 GURL::Replacements repl;
211 // Needs to stay in scope till GURL is out of scope.
212 string path(kGetUserInfoPath);
213 repl.SetPathStr(path);
214 url = url.ReplaceComponents(repl);
215
216 string post_body;
217 post_body += "LSID=";
218 post_body += EscapeUrlEncodedData(results->lsid);
219
220 unsigned long server_response_code;
221 string message_text;
222 if (!Post(url, post_body, &server_response_code, &message_text)) {
223 return false;
224 }
225
226 // Check if we received a valid AuthToken; if not, ignore it.
227 if (RC_FORBIDDEN == server_response_code) {
228 // Server says we're not authenticated.
229 ExtractAuthErrorFrom(message_text, results);
230 return false;
231 } else if (RC_REQUEST_OK == server_response_code) {
232 typedef vector<pair<string, string> > Tokens;
233 Tokens tokens;
234 base::SplitStringIntoKeyValuePairs(message_text, '=', '\n', &tokens);
235 for (Tokens::iterator i = tokens.begin(); i != tokens.end(); ++i) {
236 if ("accountType" == i->first) {
237 // We never authenticate an email as a hosted account.
238 DCHECK_EQ("GOOGLE", i->second);
239 } else if ("email" == i->first) {
240 results->primary_email = i->second;
241 }
242 }
243 return true;
244 }
245 return false;
246 }
247
GetBackoffDelaySeconds(int current_backoff_delay)248 int GaiaAuthenticator::GetBackoffDelaySeconds(int current_backoff_delay) {
249 NOTREACHED();
250 return current_backoff_delay;
251 }
252
253 // We need to call this explicitly when we need to obtain a long-lived session
254 // token.
IssueAuthToken(AuthResults * results,const string & service_id)255 bool GaiaAuthenticator::IssueAuthToken(AuthResults* results,
256 const string& service_id) {
257 DCHECK_EQ(MessageLoop::current(), message_loop_);
258 // Use the provided Gaia server, but change the path to what V1 expects.
259 GURL url(gaia_url_); // Gaia server.
260 GURL::Replacements repl;
261 // Needs to stay in scope till GURL is out of scope.
262 string path(kGaiaV1IssueAuthTokenPath);
263 repl.SetPathStr(path);
264 url = url.ReplaceComponents(repl);
265
266 string post_body;
267 post_body += "LSID=";
268 post_body += EscapeUrlEncodedData(results->lsid);
269 post_body += "&service=" + service_id;
270 post_body += "&Session=true";
271
272 unsigned long server_response_code;
273 string message_text;
274 if (!Post(url, post_body, &server_response_code, &message_text)) {
275 return false;
276 }
277
278 // Check if we received a valid AuthToken; if not, ignore it.
279 if (RC_FORBIDDEN == server_response_code) {
280 // Server says we're not authenticated.
281 ExtractAuthErrorFrom(message_text, results);
282 return false;
283 } else if (RC_REQUEST_OK == server_response_code) {
284 // Note that the format of message_text is different from what is returned
285 // in the first request, or to the sole request that is made to Gaia V2.
286 // Specifically, the entire string is the AuthToken, and looks like:
287 // "<token>" rather than "AuthToken=<token>". Thus, we need not use
288 // ExtractTokensFrom(...), but simply assign the token.
289 int last_index = message_text.length() - 1;
290 if ('\n' == message_text[last_index])
291 message_text.erase(last_index);
292 results->auth_token = message_text;
293 return true;
294 }
295 return false;
296 }
297
298 // Helper method that extracts tokens from a successful reply, and saves them
299 // in the right fields.
ExtractTokensFrom(const string & response,AuthResults * results)300 void GaiaAuthenticator::ExtractTokensFrom(const string& response,
301 AuthResults* results) {
302 vector<pair<string, string> > tokens;
303 base::SplitStringIntoKeyValuePairs(response, '=', '\n', &tokens);
304 for (vector<pair<string, string> >::iterator i = tokens.begin();
305 i != tokens.end(); ++i) {
306 if (i->first == "SID") {
307 results->sid = i->second;
308 } else if (i->first == "LSID") {
309 results->lsid = i->second;
310 } else if (i->first == "Auth") {
311 results->auth_token = i->second;
312 }
313 }
314 }
315
316 // Helper method that extracts tokens from a failure response, and saves them
317 // in the right fields.
ExtractAuthErrorFrom(const string & response,AuthResults * results)318 void GaiaAuthenticator::ExtractAuthErrorFrom(const string& response,
319 AuthResults* results) {
320 vector<pair<string, string> > tokens;
321 base::SplitStringIntoKeyValuePairs(response, '=', '\n', &tokens);
322 for (vector<pair<string, string> >::iterator i = tokens.begin();
323 i != tokens.end(); ++i) {
324 if (i->first == "Error") {
325 results->error_msg = i->second;
326 } else if (i->first == "Url") {
327 results->auth_error_url = i->second;
328 } else if (i->first == "CaptchaToken") {
329 results->captcha_token = i->second;
330 } else if (i->first == "CaptchaUrl") {
331 results->captcha_url = i->second;
332 }
333 }
334
335 // Convert string error messages to enum values. Each case has two different
336 // strings; the first one is the most current and the second one is
337 // deprecated, but available.
338 const string& error_msg = results->error_msg;
339 if (error_msg == "BadAuthentication" || error_msg == "badauth") {
340 results->auth_error = BadAuthentication;
341 } else if (error_msg == "NotVerified" || error_msg == "nv") {
342 results->auth_error = NotVerified;
343 } else if (error_msg == "TermsNotAgreed" || error_msg == "tna") {
344 results->auth_error = TermsNotAgreed;
345 } else if (error_msg == "Unknown" || error_msg == "unknown") {
346 results->auth_error = Unknown;
347 } else if (error_msg == "AccountDeleted" || error_msg == "adel") {
348 results->auth_error = AccountDeleted;
349 } else if (error_msg == "AccountDisabled" || error_msg == "adis") {
350 results->auth_error = AccountDisabled;
351 } else if (error_msg == "CaptchaRequired" || error_msg == "cr") {
352 results->auth_error = CaptchaRequired;
353 } else if (error_msg == "ServiceUnavailable" || error_msg == "ire") {
354 results->auth_error = ServiceUnavailable;
355 }
356 }
357
358 // Reset all stored credentials, perhaps in preparation for letting a different
359 // user sign in.
ResetCredentials()360 void GaiaAuthenticator::ResetCredentials() {
361 DCHECK_EQ(MessageLoop::current(), message_loop_);
362 AuthResults blank;
363 auth_results_ = blank;
364 }
365
SetUsernamePassword(const string & username,const string & password)366 void GaiaAuthenticator::SetUsernamePassword(const string& username,
367 const string& password) {
368 DCHECK_EQ(MessageLoop::current(), message_loop_);
369 auth_results_.password = password;
370 auth_results_.email = username;
371 }
372
SetUsername(const string & username)373 void GaiaAuthenticator::SetUsername(const string& username) {
374 DCHECK_EQ(MessageLoop::current(), message_loop_);
375 auth_results_.email = username;
376 }
377
RenewAuthToken(const string & auth_token)378 void GaiaAuthenticator::RenewAuthToken(const string& auth_token) {
379 DCHECK_EQ(MessageLoop::current(), message_loop_);
380 DCHECK(!this->auth_token().empty());
381 auth_results_.auth_token = auth_token;
382 }
SetAuthToken(const string & auth_token)383 void GaiaAuthenticator::SetAuthToken(const string& auth_token) {
384 DCHECK_EQ(MessageLoop::current(), message_loop_);
385 auth_results_.auth_token = auth_token;
386 }
387
Authenticate(const string & user_name,const string & password)388 bool GaiaAuthenticator::Authenticate(const string& user_name,
389 const string& password) {
390 DCHECK_EQ(MessageLoop::current(), message_loop_);
391 const string empty;
392 return Authenticate(user_name, password, empty,
393 empty);
394 }
395
396 } // namepace gaia
397
398