OLD | NEW |
(Empty) | |
| 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/browser/sync/engine/net/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_util.h" |
| 14 #include "chrome/browser/sync/engine/all_status.h" |
| 15 #include "chrome/browser/sync/engine/net/http_return.h" |
| 16 #include "chrome/browser/sync/engine/net/url_translator.h" |
| 17 #include "chrome/browser/sync/util/event_sys-inl.h" |
| 18 #include "googleurl/src/gurl.h" |
| 19 |
| 20 using std::pair; |
| 21 using std::string; |
| 22 using std::vector; |
| 23 |
| 24 // TODO(timsteele): Integrate the following two functions to string_util.h or |
| 25 // somewhere that makes them unit-testable. |
| 26 bool SplitStringIntoKeyValues(const string& line, |
| 27 char key_value_delimiter, |
| 28 string* key, vector<string>* values) { |
| 29 key->clear(); |
| 30 values->clear(); |
| 31 |
| 32 // find the key string |
| 33 int end_key_pos = line.find_first_of(key_value_delimiter); |
| 34 if (end_key_pos == string::npos) { |
| 35 DLOG(INFO) << "cannot parse key from line: " << line; |
| 36 return false; // no key |
| 37 } |
| 38 key->assign(line, 0, end_key_pos); |
| 39 |
| 40 // find the values string |
| 41 string remains(line, end_key_pos, line.size() - end_key_pos); |
| 42 int begin_values_pos = remains.find_first_not_of(key_value_delimiter); |
| 43 if (begin_values_pos == string::npos) { |
| 44 DLOG(INFO) << "cannot parse value from line: " << line; |
| 45 return false; // no value |
| 46 } |
| 47 string values_string(remains, begin_values_pos, |
| 48 remains.size() - begin_values_pos); |
| 49 |
| 50 // construct the values vector |
| 51 values->push_back(values_string); |
| 52 return true; |
| 53 } |
| 54 |
| 55 bool SplitStringIntoKeyValuePairs(const string& line, |
| 56 char key_value_delimiter, |
| 57 char key_value_pair_delimiter, |
| 58 vector<pair<string, string> >* kv_pairs) { |
| 59 kv_pairs->clear(); |
| 60 |
| 61 vector<string> pairs; |
| 62 SplitString(line, key_value_pair_delimiter, &pairs); |
| 63 |
| 64 bool success = true; |
| 65 for (size_t i = 0; i < pairs.size(); ++i) { |
| 66 string key; |
| 67 vector<string> value; |
| 68 if (!SplitStringIntoKeyValues(pairs[i], |
| 69 key_value_delimiter, |
| 70 &key, &value)) { |
| 71 // Don't return here, to allow for keys without associated |
| 72 // values; just record that our split failed. |
| 73 success = false; |
| 74 } |
| 75 DCHECK_LE(value.size(), 1); |
| 76 kv_pairs->push_back(make_pair(key, value.empty()? "" : value[0])); |
| 77 } |
| 78 return success; |
| 79 } |
| 80 |
| 81 namespace browser_sync { |
| 82 |
| 83 static const char kGaiaV1IssueAuthTokenPath[] = "/accounts/IssueAuthToken"; |
| 84 |
| 85 static const char kGetUserInfoPath[] = "/accounts/GetUserInfo"; |
| 86 |
| 87 // Sole constructor with initializers for all fields. |
| 88 GaiaAuthenticator::GaiaAuthenticator(const string& user_agent, |
| 89 const string& service_id, |
| 90 const string& gaia_url) |
| 91 : user_agent_(user_agent), |
| 92 service_id_(service_id), |
| 93 gaia_url_(gaia_url), |
| 94 request_count_(0), |
| 95 early_auth_attempt_count_(0), |
| 96 delay_(0), |
| 97 next_allowed_auth_attempt_time_(0) { |
| 98 GaiaAuthEvent done = { GaiaAuthEvent::GAIA_AUTHENTICATOR_DESTROYED, None, |
| 99 this }; |
| 100 channel_ = new Channel(done); |
| 101 } |
| 102 |
| 103 GaiaAuthenticator::~GaiaAuthenticator() { |
| 104 delete channel_; |
| 105 } |
| 106 |
| 107 bool GaiaAuthenticator::LaunchAuthenticate(const AuthParams& params, |
| 108 bool synchronous) { |
| 109 if (synchronous) |
| 110 return AuthenticateImpl(params); |
| 111 AuthParams* copy = new AuthParams; |
| 112 *copy = params; |
| 113 pthread_t thread_id; |
| 114 int result = pthread_create(&thread_id, 0, &GaiaAuthenticator::ThreadMain, |
| 115 copy); |
| 116 if (result) |
| 117 return false; |
| 118 return true; |
| 119 } |
| 120 |
| 121 |
| 122 void* GaiaAuthenticator::ThreadMain(void* arg) { |
| 123 NameCurrentThreadForDebugging("SyncEngine_GaiaAuthenticatorThread"); |
| 124 AuthParams* const params = reinterpret_cast<AuthParams*>(arg); |
| 125 params->authenticator->AuthenticateImpl(*params); |
| 126 delete params; |
| 127 return 0; |
| 128 } |
| 129 |
| 130 // mutex_ must be entered before calling this function. |
| 131 GaiaAuthenticator::AuthParams GaiaAuthenticator::MakeParams( |
| 132 const string& user_name, |
| 133 const string& password, |
| 134 SaveCredentials should_save_credentials, |
| 135 const string& captcha_token, |
| 136 const string& captcha_value, |
| 137 SignIn try_first) { |
| 138 AuthParams params; |
| 139 params.request_id = ++request_count_; |
| 140 params.email = user_name; |
| 141 params.password = password; |
| 142 params.should_save_credentials = should_save_credentials; |
| 143 params.captcha_token = captcha_token; |
| 144 params.captcha_value = captcha_value; |
| 145 params.authenticator = this; |
| 146 params.try_first = try_first; |
| 147 return params; |
| 148 } |
| 149 |
| 150 bool GaiaAuthenticator::Authenticate(const string& user_name, |
| 151 const string& password, |
| 152 SaveCredentials should_save_credentials, |
| 153 bool synchronous, |
| 154 const string& captcha_token, |
| 155 const string& captcha_value, |
| 156 SignIn try_first) { |
| 157 mutex_.Lock(); |
| 158 AuthParams const params = |
| 159 MakeParams(user_name, password, should_save_credentials, captcha_token, |
| 160 captcha_value, try_first); |
| 161 mutex_.Unlock(); |
| 162 return LaunchAuthenticate(params, synchronous); |
| 163 } |
| 164 |
| 165 bool GaiaAuthenticator::AuthenticateImpl(const AuthParams& params) { |
| 166 AuthResults results; |
| 167 const bool succeeded = AuthenticateImpl(params, &results); |
| 168 mutex_.Lock(); |
| 169 if (params.request_id == request_count_) { |
| 170 auth_results_ = results; |
| 171 GaiaAuthEvent event = { succeeded ? GaiaAuthEvent::GAIA_AUTH_SUCCEEDED |
| 172 : GaiaAuthEvent::GAIA_AUTH_FAILED, |
| 173 results.auth_error, this }; |
| 174 mutex_.Unlock(); |
| 175 channel_->NotifyListeners(event); |
| 176 } else { |
| 177 mutex_.Unlock(); |
| 178 } |
| 179 return succeeded; |
| 180 } |
| 181 |
| 182 // This method makes an HTTP request to the Gaia server, and calls other |
| 183 // methods to help parse the response. If authentication succeeded, then |
| 184 // Gaia-issued cookies are available in the respective variables; if |
| 185 // authentication failed, then the exact error is available as an enum. If the |
| 186 // client wishes to save the credentials, the last parameter must be true. |
| 187 // If a subsequent request is made with fresh credentials, the saved credentials |
| 188 // are wiped out; any subsequent request to the zero-parameter overload of this |
| 189 // method preserves the saved credentials. |
| 190 bool GaiaAuthenticator::AuthenticateImpl(const AuthParams& params, |
| 191 AuthResults* results) { |
| 192 results->credentials_saved = params.should_save_credentials; |
| 193 results->auth_error = ConnectionUnavailable; |
| 194 // Save credentials if so requested. |
| 195 if (params.should_save_credentials != DONT_SAVE_CREDENTIALS) { |
| 196 results->email = params.email.data(); |
| 197 results->password = params.password; |
| 198 } else { // Explicitly clear previously-saved credentials. |
| 199 results->email = ""; |
| 200 results->password = ""; |
| 201 } |
| 202 |
| 203 // The aim of this code is to start failing requests if due to a logic error |
| 204 // in the program we're hammering GAIA. |
| 205 time_t now = time(0); |
| 206 if (now > next_allowed_auth_attempt_time_) { |
| 207 next_allowed_auth_attempt_time_ = now + 1; |
| 208 // If we're more than 2 minutes past the allowed time we reset the early |
| 209 // attempt count. |
| 210 if (now - next_allowed_auth_attempt_time_ > 2 * 60) { |
| 211 delay_ = 1; |
| 212 early_auth_attempt_count_ = 0; |
| 213 } |
| 214 } else { |
| 215 ++early_auth_attempt_count_; |
| 216 // Allow 3 attempts, but then limit. |
| 217 if (early_auth_attempt_count_ > 3) { |
| 218 delay_ = AllStatus::GetRecommendedDelaySeconds(delay_); |
| 219 next_allowed_auth_attempt_time_ = now + delay_; |
| 220 return false; |
| 221 } |
| 222 } |
| 223 |
| 224 return PerformGaiaRequest(params, results); |
| 225 } |
| 226 |
| 227 bool GaiaAuthenticator::PerformGaiaRequest(const AuthParams& params, |
| 228 AuthResults* results) { |
| 229 GURL gaia_auth_url(gaia_url_); |
| 230 |
| 231 string post_body; |
| 232 post_body += "Email=" + CgiEscapeString(params.email); |
| 233 post_body += "&Passwd=" + CgiEscapeString(params.password); |
| 234 post_body += "&source=" + CgiEscapeString(user_agent_); |
| 235 post_body += "&service=" + service_id_; |
| 236 if (!params.captcha_token.empty() && !params.captcha_value.empty()) { |
| 237 post_body += "&logintoken=" + CgiEscapeString(params.captcha_token); |
| 238 post_body += "&logincaptcha=" + CgiEscapeString(params.captcha_value); |
| 239 } |
| 240 post_body += "&PersistentCookie=true"; |
| 241 // We set it to GOOGLE (and not HOSTED or HOSTED_OR_GOOGLE) because we only |
| 242 // allow consumer logins. |
| 243 post_body += "&accountType=GOOGLE"; |
| 244 |
| 245 string message_text; |
| 246 unsigned long server_response_code; |
| 247 if (!Post(gaia_auth_url, post_body, &server_response_code, |
| 248 &message_text)) { |
| 249 results->auth_error = ConnectionUnavailable; |
| 250 return false; |
| 251 } |
| 252 |
| 253 // Parse reply in two different ways, depending on if request failed or |
| 254 // succeeded. |
| 255 if (RC_FORBIDDEN == server_response_code) { |
| 256 ExtractAuthErrorFrom(message_text, results); |
| 257 return false; |
| 258 } else if (RC_REQUEST_OK == server_response_code) { |
| 259 ExtractTokensFrom(message_text, results); |
| 260 const bool old_gaia = |
| 261 results->auth_token.empty() && !results->lsid.empty(); |
| 262 const bool long_lived_token = |
| 263 params.should_save_credentials == PERSIST_TO_DISK; |
| 264 if ((old_gaia || long_lived_token) && |
| 265 !IssueAuthToken(results, service_id_, long_lived_token)) |
| 266 return false; |
| 267 |
| 268 return LookupEmail(results); |
| 269 } else { |
| 270 results->auth_error = Unknown; |
| 271 return false; |
| 272 } |
| 273 } |
| 274 |
| 275 bool GaiaAuthenticator::LookupEmail(AuthResults* results) { |
| 276 // Use the provided Gaia server, but change the path to what V1 expects. |
| 277 GURL url(gaia_url_); // Gaia server |
| 278 GURL::Replacements repl; |
| 279 // Needs to stay in scope till GURL is out of scope |
| 280 string path(kGetUserInfoPath); |
| 281 repl.SetPathStr(path); |
| 282 url = url.ReplaceComponents(repl); |
| 283 |
| 284 string post_body; |
| 285 post_body += "LSID="; |
| 286 post_body += CgiEscapeString(results->lsid); |
| 287 |
| 288 unsigned long server_response_code; |
| 289 string message_text; |
| 290 if (!Post(url, post_body, &server_response_code, &message_text)) { |
| 291 return false; |
| 292 } |
| 293 |
| 294 // Check if we received a valid AuthToken; if not, ignore it. |
| 295 if (RC_FORBIDDEN == server_response_code) { |
| 296 // Server says we're not authenticated. |
| 297 ExtractAuthErrorFrom(message_text, results); |
| 298 return false; |
| 299 } else if (RC_REQUEST_OK == server_response_code) { |
| 300 typedef vector<pair<string, string> > Tokens; |
| 301 Tokens tokens; |
| 302 SplitStringIntoKeyValuePairs(message_text, '=', '\n', &tokens); |
| 303 for (Tokens::iterator i = tokens.begin(); i != tokens.end(); ++i) { |
| 304 if ("accountType" == i->first) { |
| 305 // We never authenticate an email as a hosted account. |
| 306 DCHECK_EQ("GOOGLE", i->second); |
| 307 results->signin = GMAIL_SIGNIN; |
| 308 } else if ("email" == i->first) { |
| 309 results->primary_email = i->second; |
| 310 } |
| 311 } |
| 312 return true; |
| 313 } |
| 314 return false; |
| 315 } |
| 316 |
| 317 // We need to call this explicitly when we need to obtain a long-lived session |
| 318 // token. |
| 319 bool GaiaAuthenticator::IssueAuthToken(AuthResults* results, |
| 320 const string& service_id, |
| 321 bool long_lived) { |
| 322 // Use the provided Gaia server, but change the path to what V1 expects. |
| 323 GURL url(gaia_url_); // Gaia server |
| 324 GURL::Replacements repl; |
| 325 // Needs to stay in scope till GURL is out of scope |
| 326 string path(kGaiaV1IssueAuthTokenPath); |
| 327 repl.SetPathStr(path); |
| 328 url = url.ReplaceComponents(repl); |
| 329 |
| 330 string post_body; |
| 331 post_body += "LSID="; |
| 332 post_body += CgiEscapeString(results->lsid); |
| 333 post_body += "&service=" + service_id; |
| 334 if (long_lived) { |
| 335 post_body += "&Session=true"; |
| 336 } |
| 337 |
| 338 unsigned long server_response_code; |
| 339 string message_text; |
| 340 if (!Post(url, post_body, |
| 341 &server_response_code, &message_text)) { |
| 342 return false; |
| 343 } |
| 344 |
| 345 // Check if we received a valid AuthToken; if not, ignore it. |
| 346 if (RC_FORBIDDEN == server_response_code) { |
| 347 // Server says we're not authenticated. |
| 348 ExtractAuthErrorFrom(message_text, results); |
| 349 return false; |
| 350 } else if (RC_REQUEST_OK == server_response_code) { |
| 351 // Note that the format of message_text is different from what is returned |
| 352 // in the first request, or to the sole request that is made to Gaia V2. |
| 353 // Specifically, the entire string is the AuthToken, and looks like: |
| 354 // "<token>" rather than "AuthToken=<token>". Thus, we need not use |
| 355 // ExtractTokensFrom(...), but simply assign the token. |
| 356 int last_index = message_text.length() - 1; |
| 357 if ('\n' == message_text[last_index]) |
| 358 message_text.erase(last_index); |
| 359 results->auth_token = message_text; |
| 360 return true; |
| 361 } |
| 362 return false; |
| 363 } |
| 364 |
| 365 // TOOD(sync): This passing around of AuthResults makes it really unclear who |
| 366 // actually owns the authentication state and when it is valid, but this is |
| 367 // endemic to this implementation. We should fix this. |
| 368 bool GaiaAuthenticator::AuthenticateService(const string& service_id, |
| 369 const string& sid, |
| 370 const string& lsid, |
| 371 string* other_service_cookie) { |
| 372 // Copy the AuthResults structure and overload the auth_token field |
| 373 // in the copy, local_results, to mean the auth_token for service_id. |
| 374 AuthResults local_results; |
| 375 local_results.sid = sid; |
| 376 local_results.lsid = lsid; |
| 377 |
| 378 if (!IssueAuthToken(&local_results, service_id, true)) { |
| 379 LOG(ERROR) << "[AUTH] Failed to obtain cookie for " << service_id; |
| 380 return false; |
| 381 } |
| 382 |
| 383 swap(*other_service_cookie, local_results.auth_token); |
| 384 return true; |
| 385 } |
| 386 |
| 387 // Helper method that extracts tokens from a successful reply, and saves them |
| 388 // in the right fields. |
| 389 void GaiaAuthenticator::ExtractTokensFrom(const string& response, |
| 390 AuthResults* results) { |
| 391 vector<pair<string, string> > tokens; |
| 392 SplitStringIntoKeyValuePairs(response, '=', '\n', &tokens); |
| 393 for (vector<pair<string, string> >::iterator i = tokens.begin(); |
| 394 i != tokens.end(); ++i) { |
| 395 if (i->first == "SID") { |
| 396 results->sid = i->second; |
| 397 } else if (i->first == "LSID") { |
| 398 results->lsid = i->second; |
| 399 } else if (i->first == "Auth") { |
| 400 results->auth_token = i->second; |
| 401 } |
| 402 } |
| 403 } |
| 404 |
| 405 // Helper method that extracts tokens from a failure response, and saves them |
| 406 // in the right fields. |
| 407 void GaiaAuthenticator::ExtractAuthErrorFrom(const string& response, |
| 408 AuthResults* results) { |
| 409 vector<pair<string, string> > tokens; |
| 410 SplitStringIntoKeyValuePairs(response, '=', '\n', &tokens); |
| 411 for (vector<pair<string, string> >::iterator i = tokens.begin(); |
| 412 i != tokens.end(); ++i) { |
| 413 if (i->first == "Error") { |
| 414 results->error_msg = i->second; |
| 415 } else if (i->first == "Url") { |
| 416 results->auth_error_url = i->second; |
| 417 } else if (i->first == "CaptchaToken") { |
| 418 results->captcha_token = i->second; |
| 419 } else if (i->first == "CaptchaUrl") { |
| 420 results->captcha_url = i->second; |
| 421 } |
| 422 } |
| 423 |
| 424 // Convert string error messages to enum values. Each case has two different |
| 425 // strings; the first one is the most current and the second one is |
| 426 // deprecated, but available. |
| 427 const string& error_msg = results->error_msg; |
| 428 if (error_msg == "BadAuthentication" || error_msg == "badauth") { |
| 429 results->auth_error = BadAuthentication; |
| 430 } else if (error_msg == "NotVerified" || error_msg == "nv") { |
| 431 results->auth_error = NotVerified; |
| 432 } else if (error_msg == "TermsNotAgreed" || error_msg == "tna") { |
| 433 results->auth_error = TermsNotAgreed; |
| 434 } else if (error_msg == "Unknown" || error_msg == "unknown") { |
| 435 results->auth_error = Unknown; |
| 436 } else if (error_msg == "AccountDeleted" || error_msg == "adel") { |
| 437 results->auth_error = AccountDeleted; |
| 438 } else if (error_msg == "AccountDisabled" || error_msg == "adis") { |
| 439 results->auth_error = AccountDisabled; |
| 440 } else if (error_msg == "CaptchaRequired" || error_msg == "cr") { |
| 441 results->auth_error = CaptchaRequired; |
| 442 } else if (error_msg == "ServiceUnavailable" || error_msg == "ire") { |
| 443 results->auth_error = ServiceUnavailable; |
| 444 } |
| 445 } |
| 446 |
| 447 // Reset all stored credentials, perhaps in preparation for letting a different |
| 448 // user sign in. |
| 449 void GaiaAuthenticator::ResetCredentials() { |
| 450 PThreadScopedLock<PThreadMutex> enter(&mutex_); |
| 451 AuthResults blank; |
| 452 auth_results_ = blank; |
| 453 } |
| 454 |
| 455 void GaiaAuthenticator::SetUsernamePassword(const string& username, |
| 456 const string& password) { |
| 457 PThreadScopedLock<PThreadMutex> enter(&mutex_); |
| 458 auth_results_.password = password; |
| 459 auth_results_.email = username; |
| 460 } |
| 461 |
| 462 void GaiaAuthenticator::SetUsername(const string& username) { |
| 463 PThreadScopedLock<PThreadMutex> enter(&mutex_); |
| 464 auth_results_.email = username; |
| 465 } |
| 466 |
| 467 void GaiaAuthenticator::SetAuthToken(const string& auth_token, |
| 468 SaveCredentials save) { |
| 469 PThreadScopedLock<PThreadMutex> enter(&mutex_); |
| 470 auth_results_.auth_token = auth_token; |
| 471 auth_results_.credentials_saved = save; |
| 472 } |
| 473 |
| 474 bool GaiaAuthenticator::Authenticate(const string& user_name, |
| 475 const string& password, |
| 476 SaveCredentials should_save_credentials, |
| 477 bool synchronous, SignIn try_first) { |
| 478 const string empty; |
| 479 return Authenticate(user_name, password, should_save_credentials, synchronous, |
| 480 empty, empty, try_first); |
| 481 } |
| 482 |
| 483 } // namespace browser_sync |
OLD | NEW |