gaia_authenticator.cc revision 21d179b334e59e9a3bfcaed4c4430bef1bc5759d
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
19using std::pair;
20using std::string;
21using std::vector;
22
23namespace gaia {
24
25static const char kGaiaV1IssueAuthTokenPath[] = "/accounts/IssueAuthToken";
26
27static const char kGetUserInfoPath[] = "/accounts/GetUserInfo";
28
29GaiaAuthenticator::AuthResults::AuthResults() : auth_error(None) {}
30
31GaiaAuthenticator::AuthResults::~AuthResults() {}
32
33GaiaAuthenticator::AuthParams::AuthParams() : authenticator(NULL),
34                                              request_id(0) {}
35
36GaiaAuthenticator::AuthParams::~AuthParams() {}
37
38// Sole constructor with initializers for all fields.
39GaiaAuthenticator::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
55GaiaAuthenticator::~GaiaAuthenticator() {
56  delete channel_;
57}
58
59// mutex_ must be entered before calling this function.
60GaiaAuthenticator::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
75bool 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
86bool 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
97bool 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.
119bool 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
155bool 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
199bool 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
206bool 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
248int 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.
255bool 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.
300void 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.
318void 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.
360void GaiaAuthenticator::ResetCredentials() {
361  DCHECK_EQ(MessageLoop::current(), message_loop_);
362  AuthResults blank;
363  auth_results_ = blank;
364}
365
366void 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
373void GaiaAuthenticator::SetUsername(const string& username) {
374  DCHECK_EQ(MessageLoop::current(), message_loop_);
375  auth_results_.email = username;
376}
377
378void 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}
383void GaiaAuthenticator::SetAuthToken(const string& auth_token) {
384  DCHECK_EQ(MessageLoop::current(), message_loop_);
385  auth_results_.auth_token = auth_token;
386}
387
388bool 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