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