// Copyright (c) 2009 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/common/net/gaia/gaia_authenticator.h" #include <string> #include <utility> #include <vector> #include "base/basictypes.h" #include "base/port.h" #include "base/string_split.h" #include "chrome/common/deprecated/event_sys-inl.h" #include "chrome/common/net/http_return.h" #include "googleurl/src/gurl.h" #include "net/base/escape.h" using std::pair; using std::string; using std::vector; namespace gaia { static const char kGaiaV1IssueAuthTokenPath[] = "/accounts/IssueAuthToken"; static const char kGetUserInfoPath[] = "/accounts/GetUserInfo"; GaiaAuthenticator::AuthResults::AuthResults() : auth_error(None) {} GaiaAuthenticator::AuthResults::~AuthResults() {} GaiaAuthenticator::AuthParams::AuthParams() : authenticator(NULL), request_id(0) {} GaiaAuthenticator::AuthParams::~AuthParams() {} // Sole constructor with initializers for all fields. GaiaAuthenticator::GaiaAuthenticator(const string& user_agent, const string& service_id, const string& gaia_url) : user_agent_(user_agent), service_id_(service_id), gaia_url_(gaia_url), request_count_(0), delay_(0), next_allowed_auth_attempt_time_(0), early_auth_attempt_count_(0), message_loop_(NULL) { GaiaAuthEvent done = { GaiaAuthEvent::GAIA_AUTHENTICATOR_DESTROYED, None, this }; channel_ = new Channel(done); } GaiaAuthenticator::~GaiaAuthenticator() { delete channel_; } // mutex_ must be entered before calling this function. GaiaAuthenticator::AuthParams GaiaAuthenticator::MakeParams( const string& user_name, const string& password, const string& captcha_token, const string& captcha_value) { AuthParams params; params.request_id = ++request_count_; params.email = user_name; params.password = password; params.captcha_token = captcha_token; params.captcha_value = captcha_value; params.authenticator = this; return params; } bool GaiaAuthenticator::Authenticate(const string& user_name, const string& password, const string& captcha_token, const string& captcha_value) { DCHECK_EQ(MessageLoop::current(), message_loop_); AuthParams const params = MakeParams(user_name, password, captcha_token, captcha_value); return AuthenticateImpl(params); } bool GaiaAuthenticator::AuthenticateWithLsid(const string& lsid) { auth_results_.lsid = lsid; // We need to lookup the email associated with this LSID cookie in order to // update |auth_results_| with the correct values. if (LookupEmail(&auth_results_)) { auth_results_.email = auth_results_.primary_email; return IssueAuthToken(&auth_results_, service_id_); } return false; } bool GaiaAuthenticator::AuthenticateImpl(const AuthParams& params) { DCHECK_EQ(MessageLoop::current(), message_loop_); AuthResults results; const bool succeeded = AuthenticateImpl(params, &results); if (params.request_id == request_count_) { auth_results_ = results; GaiaAuthEvent event = { succeeded ? GaiaAuthEvent::GAIA_AUTH_SUCCEEDED : GaiaAuthEvent::GAIA_AUTH_FAILED, results.auth_error, this }; channel_->NotifyListeners(event); } return succeeded; } // This method makes an HTTP request to the Gaia server, and calls other // methods to help parse the response. If authentication succeeded, then // Gaia-issued cookies are available in the respective variables; if // authentication failed, then the exact error is available as an enum. If the // client wishes to save the credentials, the last parameter must be true. // If a subsequent request is made with fresh credentials, the saved credentials // are wiped out; any subsequent request to the zero-parameter overload of this // method preserves the saved credentials. bool GaiaAuthenticator::AuthenticateImpl(const AuthParams& params, AuthResults* results) { DCHECK_EQ(MessageLoop::current(), message_loop_); results->auth_error = ConnectionUnavailable; results->email = params.email.data(); results->password = params.password; // The aim of this code is to start failing requests if due to a logic error // in the program we're hammering GAIA. #if defined(OS_WIN) __time32_t now = _time32(0); #else // defined(OS_WIN) time_t now = time(0); #endif // defined(OS_WIN) if (now > next_allowed_auth_attempt_time_) { next_allowed_auth_attempt_time_ = now + 1; // If we're more than 2 minutes past the allowed time we reset the early // attempt count. if (now - next_allowed_auth_attempt_time_ > 2 * 60) { delay_ = 1; early_auth_attempt_count_ = 0; } } else { ++early_auth_attempt_count_; // Allow 3 attempts, but then limit. if (early_auth_attempt_count_ > 3) { delay_ = GetBackoffDelaySeconds(delay_); next_allowed_auth_attempt_time_ = now + delay_; return false; } } return PerformGaiaRequest(params, results); } bool GaiaAuthenticator::PerformGaiaRequest(const AuthParams& params, AuthResults* results) { DCHECK_EQ(MessageLoop::current(), message_loop_); GURL gaia_auth_url(gaia_url_); string post_body; post_body += "Email=" + EscapeUrlEncodedData(params.email); post_body += "&Passwd=" + EscapeUrlEncodedData(params.password); post_body += "&source=" + EscapeUrlEncodedData(user_agent_); post_body += "&service=" + service_id_; if (!params.captcha_token.empty() && !params.captcha_value.empty()) { post_body += "&logintoken=" + EscapeUrlEncodedData(params.captcha_token); post_body += "&logincaptcha=" + EscapeUrlEncodedData(params.captcha_value); } post_body += "&PersistentCookie=true"; // We set it to GOOGLE (and not HOSTED or HOSTED_OR_GOOGLE) because we only // allow consumer logins. post_body += "&accountType=GOOGLE"; string message_text; unsigned long server_response_code; if (!Post(gaia_auth_url, post_body, &server_response_code, &message_text)) { results->auth_error = ConnectionUnavailable; return false; } // Parse reply in two different ways, depending on if request failed or // succeeded. if (RC_FORBIDDEN == server_response_code) { ExtractAuthErrorFrom(message_text, results); return false; } else if (RC_REQUEST_OK == server_response_code) { ExtractTokensFrom(message_text, results); if (!IssueAuthToken(results, service_id_)) { return false; } return LookupEmail(results); } else { results->auth_error = Unknown; return false; } } bool GaiaAuthenticator::Post(const GURL& url, const std::string& post_body, unsigned long* response_code, std::string* response_body) { return false; } bool GaiaAuthenticator::LookupEmail(AuthResults* results) { DCHECK_EQ(MessageLoop::current(), message_loop_); // Use the provided Gaia server, but change the path to what V1 expects. GURL url(gaia_url_); // Gaia server. GURL::Replacements repl; // Needs to stay in scope till GURL is out of scope. string path(kGetUserInfoPath); repl.SetPathStr(path); url = url.ReplaceComponents(repl); string post_body; post_body += "LSID="; post_body += EscapeUrlEncodedData(results->lsid); unsigned long server_response_code; string message_text; if (!Post(url, post_body, &server_response_code, &message_text)) { return false; } // Check if we received a valid AuthToken; if not, ignore it. if (RC_FORBIDDEN == server_response_code) { // Server says we're not authenticated. ExtractAuthErrorFrom(message_text, results); return false; } else if (RC_REQUEST_OK == server_response_code) { typedef vector<pair<string, string> > Tokens; Tokens tokens; base::SplitStringIntoKeyValuePairs(message_text, '=', '\n', &tokens); for (Tokens::iterator i = tokens.begin(); i != tokens.end(); ++i) { if ("accountType" == i->first) { // We never authenticate an email as a hosted account. DCHECK_EQ("GOOGLE", i->second); } else if ("email" == i->first) { results->primary_email = i->second; } } return true; } return false; } int GaiaAuthenticator::GetBackoffDelaySeconds(int current_backoff_delay) { NOTREACHED(); return current_backoff_delay; } // We need to call this explicitly when we need to obtain a long-lived session // token. bool GaiaAuthenticator::IssueAuthToken(AuthResults* results, const string& service_id) { DCHECK_EQ(MessageLoop::current(), message_loop_); // Use the provided Gaia server, but change the path to what V1 expects. GURL url(gaia_url_); // Gaia server. GURL::Replacements repl; // Needs to stay in scope till GURL is out of scope. string path(kGaiaV1IssueAuthTokenPath); repl.SetPathStr(path); url = url.ReplaceComponents(repl); string post_body; post_body += "LSID="; post_body += EscapeUrlEncodedData(results->lsid); post_body += "&service=" + service_id; post_body += "&Session=true"; unsigned long server_response_code; string message_text; if (!Post(url, post_body, &server_response_code, &message_text)) { return false; } // Check if we received a valid AuthToken; if not, ignore it. if (RC_FORBIDDEN == server_response_code) { // Server says we're not authenticated. ExtractAuthErrorFrom(message_text, results); return false; } else if (RC_REQUEST_OK == server_response_code) { // Note that the format of message_text is different from what is returned // in the first request, or to the sole request that is made to Gaia V2. // Specifically, the entire string is the AuthToken, and looks like: // "<token>" rather than "AuthToken=<token>". Thus, we need not use // ExtractTokensFrom(...), but simply assign the token. int last_index = message_text.length() - 1; if ('\n' == message_text[last_index]) message_text.erase(last_index); results->auth_token = message_text; return true; } return false; } // Helper method that extracts tokens from a successful reply, and saves them // in the right fields. void GaiaAuthenticator::ExtractTokensFrom(const string& response, AuthResults* results) { vector<pair<string, string> > tokens; base::SplitStringIntoKeyValuePairs(response, '=', '\n', &tokens); for (vector<pair<string, string> >::iterator i = tokens.begin(); i != tokens.end(); ++i) { if (i->first == "SID") { results->sid = i->second; } else if (i->first == "LSID") { results->lsid = i->second; } else if (i->first == "Auth") { results->auth_token = i->second; } } } // Helper method that extracts tokens from a failure response, and saves them // in the right fields. void GaiaAuthenticator::ExtractAuthErrorFrom(const string& response, AuthResults* results) { vector<pair<string, string> > tokens; base::SplitStringIntoKeyValuePairs(response, '=', '\n', &tokens); for (vector<pair<string, string> >::iterator i = tokens.begin(); i != tokens.end(); ++i) { if (i->first == "Error") { results->error_msg = i->second; } else if (i->first == "Url") { results->auth_error_url = i->second; } else if (i->first == "CaptchaToken") { results->captcha_token = i->second; } else if (i->first == "CaptchaUrl") { results->captcha_url = i->second; } } // Convert string error messages to enum values. Each case has two different // strings; the first one is the most current and the second one is // deprecated, but available. const string& error_msg = results->error_msg; if (error_msg == "BadAuthentication" || error_msg == "badauth") { results->auth_error = BadAuthentication; } else if (error_msg == "NotVerified" || error_msg == "nv") { results->auth_error = NotVerified; } else if (error_msg == "TermsNotAgreed" || error_msg == "tna") { results->auth_error = TermsNotAgreed; } else if (error_msg == "Unknown" || error_msg == "unknown") { results->auth_error = Unknown; } else if (error_msg == "AccountDeleted" || error_msg == "adel") { results->auth_error = AccountDeleted; } else if (error_msg == "AccountDisabled" || error_msg == "adis") { results->auth_error = AccountDisabled; } else if (error_msg == "CaptchaRequired" || error_msg == "cr") { results->auth_error = CaptchaRequired; } else if (error_msg == "ServiceUnavailable" || error_msg == "ire") { results->auth_error = ServiceUnavailable; } } // Reset all stored credentials, perhaps in preparation for letting a different // user sign in. void GaiaAuthenticator::ResetCredentials() { DCHECK_EQ(MessageLoop::current(), message_loop_); AuthResults blank; auth_results_ = blank; } void GaiaAuthenticator::SetUsernamePassword(const string& username, const string& password) { DCHECK_EQ(MessageLoop::current(), message_loop_); auth_results_.password = password; auth_results_.email = username; } void GaiaAuthenticator::SetUsername(const string& username) { DCHECK_EQ(MessageLoop::current(), message_loop_); auth_results_.email = username; } void GaiaAuthenticator::RenewAuthToken(const string& auth_token) { DCHECK_EQ(MessageLoop::current(), message_loop_); DCHECK(!this->auth_token().empty()); auth_results_.auth_token = auth_token; } void GaiaAuthenticator::SetAuthToken(const string& auth_token) { DCHECK_EQ(MessageLoop::current(), message_loop_); auth_results_.auth_token = auth_token; } bool GaiaAuthenticator::Authenticate(const string& user_name, const string& password) { DCHECK_EQ(MessageLoop::current(), message_loop_); const string empty; return Authenticate(user_name, password, empty, empty); } } // namepace gaia