// Copyright (c) 2006-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/browser/sync/engine/auth_watcher.h" #include "base/file_util.h" #include "base/string_util.h" #include "chrome/browser/sync/engine/all_status.h" #include "chrome/browser/sync/engine/authenticator.h" #include "chrome/browser/sync/engine/net/gaia_authenticator.h" #include "chrome/browser/sync/engine/net/server_connection_manager.h" #include "chrome/browser/sync/notifier/listener/talk_mediator.h" #include "chrome/browser/sync/protocol/service_constants.h" #include "chrome/browser/sync/syncable/directory_manager.h" #include "chrome/browser/sync/syncable/syncable.h" #include "chrome/browser/sync/util/character_set_converters.h" #include "chrome/browser/sync/util/event_sys-inl.h" #include "chrome/browser/sync/util/pthread_helpers.h" #include "chrome/browser/sync/util/user_settings.h" // How authentication happens: // // Kick Off: // The sync API looks to see if the user's name and // password are stored. If so, it calls authwatcher.Authenticate() with // them. Otherwise it fires an error event. // // On failed Gaia Auth: // The AuthWatcher attempts to use saved hashes to authenticate // locally, and on success opens the share. // On failure, fires an error event. // // On successful Gaia Auth: // AuthWatcher launches a thread to open the share and to get the // authentication token from the sync server. using std::pair; using std::string; using std::vector; namespace browser_sync { AuthWatcher::AuthWatcher(DirectoryManager* dirman, ServerConnectionManager* scm, AllStatus* allstatus, const string& user_agent, const string& service_id, const string& gaia_url, UserSettings* user_settings, GaiaAuthenticator* gaia_auth, TalkMediator* talk_mediator) : dirman_(dirman), scm_(scm), allstatus_(allstatus), status_(NOT_AUTHENTICATED), thread_handle_valid_(false), authenticating_now_(false), current_attempt_trigger_(AuthWatcherEvent::USER_INITIATED), user_settings_(user_settings), gaia_(gaia_auth), talk_mediator_(talk_mediator) { connmgr_hookup_.reset( NewEventListenerHookup(scm->channel(), this, &AuthWatcher::HandleServerConnectionEvent)); AuthWatcherEvent done = { AuthWatcherEvent::AUTHWATCHER_DESTROYED }; channel_.reset(new Channel(done)); } void* AuthWatcher::AuthenticationThreadStartRoutine(void* arg) { ThreadParams* args = reinterpret_cast(arg); return args->self->AuthenticationThreadMain(args); } bool AuthWatcher::ProcessGaiaAuthSuccess() { GaiaAuthenticator::AuthResults results = gaia_->results(); // We just successfully signed in again, let's clear out any residual cached // login data from earlier sessions. ClearAuthenticationData(); user_settings_->StoreEmailForSignin(results.email, results.primary_email); user_settings_->RememberSigninType(results.email, results.signin); user_settings_->RememberSigninType(results.primary_email, results.signin); results.email = results.primary_email; gaia_->SetUsernamePassword(results.primary_email, results.password); if (!user_settings_->VerifyAgainstStoredHash(results.email, results.password)) user_settings_->StoreHashedPassword(results.email, results.password); if (PERSIST_TO_DISK == results.credentials_saved) { user_settings_->SetAuthTokenForService(results.email, SYNC_SERVICE_NAME, gaia_->auth_token()); } return AuthenticateWithToken(results.email, gaia_->auth_token()); } bool AuthWatcher::GetAuthTokenForService(const string& service_name, string* service_token) { string user_name; // We special case this one by trying to return it from memory first. We // do this because the user may not have checked "Remember me" and so we // may not have persisted the sync service token beyond the initial // login. if (SYNC_SERVICE_NAME == service_name && !sync_service_token_.empty()) { *service_token = sync_service_token_; return true; } if (user_settings_->GetLastUserAndServiceToken(service_name, &user_name, service_token)) { // The casing gets preserved in some places and not in others it seems, // at least I have observed different casings persisted to different DB // tables. if (!base::strcasecmp(user_name.c_str(), user_settings_->email().c_str())) { return true; } else { LOG(ERROR) << "ERROR: We seem to have saved credentials for someone " << " other than the current user."; return false; } } return false; } const char kAuthWatcher[] = "AuthWatcher"; bool AuthWatcher::AuthenticateWithToken(const string& gaia_email, const string& auth_token) { // Store a copy of the sync service token in memory. sync_service_token_ = auth_token; scm_->set_auth_token(sync_service_token_); Authenticator auth(scm_, user_settings_); Authenticator::AuthenticationResult result = auth.AuthenticateToken(auth_token); string email = gaia_email; if (auth.display_email() && *auth.display_email()) { email = auth.display_email(); LOG(INFO) << "Auth returned email " << email << " for gaia email " << gaia_email; } AuthWatcherEvent event = {AuthWatcherEvent::ILLEGAL_VALUE , 0}; gaia_->SetUsername(email); gaia_->SetAuthToken(auth_token, SAVE_IN_MEMORY_ONLY); const bool was_authenticated = NOT_AUTHENTICATED != status_; switch (result) { case Authenticator::SUCCESS: { status_ = GAIA_AUTHENTICATED; PathString share_name; CHECK(AppendUTF8ToPathString(email.data(), email.size(), &share_name)); user_settings_->SwitchUser(email); // Set the authentication token for notifications talk_mediator_->SetAuthToken(email, auth_token); if (!was_authenticated) LoadDirectoryListAndOpen(share_name); NotifyAuthSucceeded(email); return true; } case Authenticator::BAD_AUTH_TOKEN: event.what_happened = AuthWatcherEvent::SERVICE_AUTH_FAILED; break; case Authenticator::CORRUPT_SERVER_RESPONSE: case Authenticator::SERVICE_DOWN: event.what_happened = AuthWatcherEvent::SERVICE_CONNECTION_FAILED; break; case Authenticator::USER_NOT_ACTIVATED: event.what_happened = AuthWatcherEvent::SERVICE_USER_NOT_SIGNED_UP; break; default: LOG(FATAL) << "Illegal return from AuthenticateToken"; return true; // keep the compiler happy } // Always fall back to local authentication. if (was_authenticated || AuthenticateLocally(email)) { if (AuthWatcherEvent::SERVICE_CONNECTION_FAILED == event.what_happened) return true; } CHECK(event.what_happened != AuthWatcherEvent::ILLEGAL_VALUE); NotifyListeners(&event); return true; } bool AuthWatcher::AuthenticateLocally(string email) { user_settings_->GetEmailForSignin(&email); if (file_util::PathExists(dirman_->GetSyncDataDatabasePath())) { gaia_->SetUsername(email); status_ = LOCALLY_AUTHENTICATED; user_settings_->SwitchUser(email); PathString share_name; CHECK(AppendUTF8ToPathString(email.data(), email.size(), &share_name)); LoadDirectoryListAndOpen(share_name); NotifyAuthSucceeded(email); return true; } else { return false; } } bool AuthWatcher::AuthenticateLocally(string email, const string& password) { user_settings_->GetEmailForSignin(&email); return user_settings_->VerifyAgainstStoredHash(email, password) && AuthenticateLocally(email); } void AuthWatcher::ProcessGaiaAuthFailure() { GaiaAuthenticator::AuthResults results = gaia_->results(); if (LOCALLY_AUTHENTICATED == status_) { return; // nothing todo } else if (AuthenticateLocally(results.email, results.password)) { // We save the "Remember me" checkbox by putting a non-null auth // token into the last_user table. So if we're offline and the // user checks the box, insert a bogus auth token. if (PERSIST_TO_DISK == results.credentials_saved) { const string auth_token("bogus"); user_settings_->SetAuthTokenForService(results.email, SYNC_SERVICE_NAME, auth_token); } const bool unavailable = ConnectionUnavailable == results.auth_error || Unknown == results.auth_error || ServiceUnavailable == results.auth_error; if (unavailable) return; } AuthWatcherEvent myevent = { AuthWatcherEvent::GAIA_AUTH_FAILED, &results }; NotifyListeners(&myevent); } void* AuthWatcher::AuthenticationThreadMain(ThreadParams* args) { NameCurrentThreadForDebugging("SyncEngine_AuthWatcherThread"); { // This short lock ensures our launching function (StartNewAuthAttempt) is // done. MutexLock lock(&mutex_); current_attempt_trigger_ = args->trigger; } SaveCredentials save = args->persist_creds_to_disk ? PERSIST_TO_DISK : SAVE_IN_MEMORY_ONLY; int attempt = 0; SignIn const signin = user_settings_-> RecallSigninType(args->email, GMAIL_SIGNIN); if (!args->password.empty()) while (true) { bool authenticated; if (!args->captcha_token.empty() && !args->captcha_value.empty()) authenticated = gaia_->Authenticate(args->email, args->password, save, true, args->captcha_token, args->captcha_value, signin); else authenticated = gaia_->Authenticate(args->email, args->password, save, true, signin); if (authenticated) { if (!ProcessGaiaAuthSuccess()) { if (3 != ++attempt) continue; AuthWatcherEvent event = { AuthWatcherEvent::SERVICE_CONNECTION_FAILED, 0 }; NotifyListeners(&event); } } else { ProcessGaiaAuthFailure(); } break; } else if (!args->auth_token.empty()) { AuthenticateWithToken(args->email, args->auth_token); } else { LOG(ERROR) << "Attempt to authenticate with no credentials."; } { MutexLock lock(&mutex_); authenticating_now_ = false; } delete args; return 0; } void AuthWatcher::Reset() { status_ = NOT_AUTHENTICATED; } void AuthWatcher::NotifyAuthSucceeded(const string& email) { LOG(INFO) << "NotifyAuthSucceeded"; AuthWatcherEvent event = { AuthWatcherEvent::AUTH_SUCCEEDED }; event.user_email = email; NotifyListeners(&event); } bool AuthWatcher::StartNewAuthAttempt(const string& email, const string& password, const string& auth_token, const string& captcha_token, const string& captcha_value, bool persist_creds_to_disk, AuthWatcherEvent::AuthenticationTrigger trigger) { AuthWatcherEvent event = { AuthWatcherEvent::AUTHENTICATION_ATTEMPT_START }; NotifyListeners(&event); MutexLock lock(&mutex_); if (authenticating_now_) return false; if (thread_handle_valid_) { int join_return = pthread_join(thread_, 0); if (0 != join_return) LOG(ERROR) << "pthread_join failed returning " << join_return; } string mail = email; if (email.find('@') == string::npos) { mail.push_back('@'); // TODO(chron): Should this be done only at the UI level? mail.append(DEFAULT_SIGNIN_DOMAIN); } ThreadParams* args = new ThreadParams; args->self = this; args->email = mail; args->password = password; args->auth_token = auth_token; args->captcha_token = captcha_token; args->captcha_value = captcha_value; args->persist_creds_to_disk = persist_creds_to_disk; args->trigger = trigger; if (0 != pthread_create(&thread_, NULL, AuthenticationThreadStartRoutine, args)) { LOG(ERROR) << "Failed to create auth thread."; return false; } authenticating_now_ = true; thread_handle_valid_ = true; return true; } void AuthWatcher::WaitForAuthThreadFinish() { { MutexLock lock(&mutex_); if (!thread_handle_valid_) return; } pthread_join(thread_, 0); } void AuthWatcher::HandleServerConnectionEvent( const ServerConnectionEvent& event) { if (event.server_reachable && !authenticating_now_ && (event.connection_code == HttpResponse::SYNC_AUTH_ERROR || status_ == LOCALLY_AUTHENTICATED)) { // We're either online or just got reconnected and want to try to // authenticate. If we've got a saved token this should just work. If not // the auth failure should trigger UI indications that we're not logged in. // METRIC: If we get a SYNC_AUTH_ERROR, our token expired. GaiaAuthenticator::AuthResults authresults = gaia_->results(); if (!StartNewAuthAttempt(authresults.email, authresults.password, authresults.auth_token, "", "", PERSIST_TO_DISK == authresults.credentials_saved, AuthWatcherEvent::EXPIRED_CREDENTIALS)) LOG(INFO) << "Couldn't start a new auth attempt."; } } bool AuthWatcher::LoadDirectoryListAndOpen(const PathString& login) { LOG(INFO) << "LoadDirectoryListAndOpen(" << login << ")"; bool initial_sync_ended = false; dirman_->Open(login); syncable::ScopedDirLookup dir(dirman_, login); if (dir.good() && dir->initial_sync_ended()) initial_sync_ended = true; LOG(INFO) << "LoadDirectoryListAndOpen returning " << initial_sync_ended; return initial_sync_ended; } AuthWatcher::~AuthWatcher() { WaitForAuthThreadFinish(); } void AuthWatcher::Authenticate(const string& email, const string& password, const string& captcha_token, const string& captcha_value, bool persist_creds_to_disk) { LOG(INFO) << "AuthWatcher::Authenticate called"; WaitForAuthThreadFinish(); // We CHECK here because WaitForAuthThreadFinish should ensure there's no // ongoing auth attempt. string empty; CHECK(StartNewAuthAttempt(email, password, empty, captcha_token, captcha_value, persist_creds_to_disk, AuthWatcherEvent::USER_INITIATED)); } void AuthWatcher::Logout() { scm_->ResetAuthStatus(); Reset(); WaitForAuthThreadFinish(); ClearAuthenticationData(); } void AuthWatcher::ClearAuthenticationData() { sync_service_token_.clear(); scm_->set_auth_token(sync_service_token()); user_settings_->ClearAllServiceTokens(); } string AuthWatcher::email() const { return gaia_->email(); } void AuthWatcher::NotifyListeners(AuthWatcherEvent* event) { event->trigger = current_attempt_trigger_; channel_->NotifyListeners(*event); } } // namespace browser_sync