OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2006-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/auth_watcher.h" |
| 6 |
| 7 #include "base/file_util.h" |
| 8 #include "base/string_util.h" |
| 9 #include "chrome/browser/sync/engine/all_status.h" |
| 10 #include "chrome/browser/sync/engine/authenticator.h" |
| 11 #include "chrome/browser/sync/engine/net/gaia_authenticator.h" |
| 12 #include "chrome/browser/sync/engine/net/server_connection_manager.h" |
| 13 #include "chrome/browser/sync/notifier/listener/talk_mediator.h" |
| 14 #include "chrome/browser/sync/protocol/service_constants.h" |
| 15 #include "chrome/browser/sync/syncable/directory_manager.h" |
| 16 #include "chrome/browser/sync/syncable/syncable.h" |
| 17 #include "chrome/browser/sync/util/character_set_converters.h" |
| 18 #include "chrome/browser/sync/util/event_sys-inl.h" |
| 19 #include "chrome/browser/sync/util/pthread_helpers.h" |
| 20 #include "chrome/browser/sync/util/user_settings.h" |
| 21 |
| 22 // How authentication happens: |
| 23 // |
| 24 // Kick Off: |
| 25 // The sync API looks to see if the user's name and |
| 26 // password are stored. If so, it calls authwatcher.Authenticate() with |
| 27 // them. Otherwise it fires an error event. |
| 28 // |
| 29 // On failed Gaia Auth: |
| 30 // The AuthWatcher attempts to use saved hashes to authenticate |
| 31 // locally, and on success opens the share. |
| 32 // On failure, fires an error event. |
| 33 // |
| 34 // On successful Gaia Auth: |
| 35 // AuthWatcher launches a thread to open the share and to get the |
| 36 // authentication token from the sync server. |
| 37 |
| 38 using std::pair; |
| 39 using std::string; |
| 40 using std::vector; |
| 41 |
| 42 namespace browser_sync { |
| 43 |
| 44 AuthWatcher::AuthWatcher(DirectoryManager* dirman, |
| 45 ServerConnectionManager* scm, |
| 46 AllStatus* allstatus, |
| 47 const string& user_agent, |
| 48 const string& service_id, |
| 49 const string& gaia_url, |
| 50 UserSettings* user_settings, |
| 51 GaiaAuthenticator* gaia_auth, |
| 52 TalkMediator* talk_mediator) |
| 53 : dirman_(dirman), |
| 54 scm_(scm), |
| 55 allstatus_(allstatus), |
| 56 status_(NOT_AUTHENTICATED), |
| 57 thread_handle_valid_(false), |
| 58 authenticating_now_(false), |
| 59 current_attempt_trigger_(AuthWatcherEvent::USER_INITIATED), |
| 60 user_settings_(user_settings), |
| 61 gaia_(gaia_auth), |
| 62 talk_mediator_(talk_mediator) { |
| 63 connmgr_hookup_.reset( |
| 64 NewEventListenerHookup(scm->channel(), this, |
| 65 &AuthWatcher::HandleServerConnectionEvent)); |
| 66 AuthWatcherEvent done = { AuthWatcherEvent::AUTHWATCHER_DESTROYED }; |
| 67 channel_.reset(new Channel(done)); |
| 68 } |
| 69 |
| 70 void* AuthWatcher::AuthenticationThreadStartRoutine(void* arg) { |
| 71 ThreadParams* args = reinterpret_cast<ThreadParams*>(arg); |
| 72 return args->self->AuthenticationThreadMain(args); |
| 73 } |
| 74 |
| 75 bool AuthWatcher::ProcessGaiaAuthSuccess() { |
| 76 GaiaAuthenticator::AuthResults results = gaia_->results(); |
| 77 |
| 78 // We just successfully signed in again, let's clear out any residual cached |
| 79 // login data from earlier sessions. |
| 80 ClearAuthenticationData(); |
| 81 |
| 82 user_settings_->StoreEmailForSignin(results.email, results.primary_email); |
| 83 user_settings_->RememberSigninType(results.email, results.signin); |
| 84 user_settings_->RememberSigninType(results.primary_email, results.signin); |
| 85 results.email = results.primary_email; |
| 86 gaia_->SetUsernamePassword(results.primary_email, results.password); |
| 87 if (!user_settings_->VerifyAgainstStoredHash(results.email, results.password)) |
| 88 user_settings_->StoreHashedPassword(results.email, results.password); |
| 89 |
| 90 if (PERSIST_TO_DISK == results.credentials_saved) { |
| 91 user_settings_->SetAuthTokenForService(results.email, |
| 92 SYNC_SERVICE_NAME, |
| 93 gaia_->auth_token()); |
| 94 } |
| 95 |
| 96 return AuthenticateWithToken(results.email, gaia_->auth_token()); |
| 97 } |
| 98 |
| 99 bool AuthWatcher::GetAuthTokenForService(const string& service_name, |
| 100 string* service_token) { |
| 101 string user_name; |
| 102 |
| 103 // We special case this one by trying to return it from memory first. We |
| 104 // do this because the user may not have checked "Remember me" and so we |
| 105 // may not have persisted the sync service token beyond the initial |
| 106 // login. |
| 107 if (SYNC_SERVICE_NAME == service_name && !sync_service_token_.empty()) { |
| 108 *service_token = sync_service_token_; |
| 109 return true; |
| 110 } |
| 111 |
| 112 if (user_settings_->GetLastUserAndServiceToken(service_name, &user_name, |
| 113 service_token)) { |
| 114 // The casing gets preserved in some places and not in others it seems, |
| 115 // at least I have observed different casings persisted to different DB |
| 116 // tables. |
| 117 if (!base::strcasecmp(user_name.c_str(), |
| 118 user_settings_->email().c_str())) { |
| 119 return true; |
| 120 } else { |
| 121 LOG(ERROR) << "ERROR: We seem to have saved credentials for someone " |
| 122 << " other than the current user."; |
| 123 return false; |
| 124 } |
| 125 } |
| 126 |
| 127 return false; |
| 128 } |
| 129 |
| 130 const char kAuthWatcher[] = "AuthWatcher"; |
| 131 |
| 132 bool AuthWatcher::AuthenticateWithToken(const string& gaia_email, |
| 133 const string& auth_token) { |
| 134 // Store a copy of the sync service token in memory. |
| 135 sync_service_token_ = auth_token; |
| 136 scm_->set_auth_token(sync_service_token_); |
| 137 |
| 138 Authenticator auth(scm_, user_settings_); |
| 139 Authenticator::AuthenticationResult result = |
| 140 auth.AuthenticateToken(auth_token); |
| 141 string email = gaia_email; |
| 142 if (auth.display_email() && *auth.display_email()) { |
| 143 email = auth.display_email(); |
| 144 LOG(INFO) << "Auth returned email " << email << " for gaia email " << |
| 145 gaia_email; |
| 146 } |
| 147 AuthWatcherEvent event = {AuthWatcherEvent::ILLEGAL_VALUE , 0}; |
| 148 gaia_->SetUsername(email); |
| 149 gaia_->SetAuthToken(auth_token, SAVE_IN_MEMORY_ONLY); |
| 150 const bool was_authenticated = NOT_AUTHENTICATED != status_; |
| 151 switch (result) { |
| 152 case Authenticator::SUCCESS: |
| 153 { |
| 154 status_ = GAIA_AUTHENTICATED; |
| 155 PathString share_name; |
| 156 CHECK(AppendUTF8ToPathString(email.data(), email.size(), &share_name)); |
| 157 user_settings_->SwitchUser(email); |
| 158 |
| 159 // Set the authentication token for notifications |
| 160 talk_mediator_->SetAuthToken(email, auth_token); |
| 161 |
| 162 if (!was_authenticated) |
| 163 LoadDirectoryListAndOpen(share_name); |
| 164 NotifyAuthSucceeded(email); |
| 165 return true; |
| 166 } |
| 167 case Authenticator::BAD_AUTH_TOKEN: |
| 168 event.what_happened = AuthWatcherEvent::SERVICE_AUTH_FAILED; |
| 169 break; |
| 170 case Authenticator::CORRUPT_SERVER_RESPONSE: |
| 171 case Authenticator::SERVICE_DOWN: |
| 172 event.what_happened = AuthWatcherEvent::SERVICE_CONNECTION_FAILED; |
| 173 break; |
| 174 case Authenticator::USER_NOT_ACTIVATED: |
| 175 event.what_happened = AuthWatcherEvent::SERVICE_USER_NOT_SIGNED_UP; |
| 176 break; |
| 177 default: |
| 178 LOG(FATAL) << "Illegal return from AuthenticateToken"; |
| 179 return true; // keep the compiler happy |
| 180 } |
| 181 // Always fall back to local authentication. |
| 182 if (was_authenticated || AuthenticateLocally(email)) { |
| 183 if (AuthWatcherEvent::SERVICE_CONNECTION_FAILED == event.what_happened) |
| 184 return true; |
| 185 } |
| 186 CHECK(event.what_happened != AuthWatcherEvent::ILLEGAL_VALUE); |
| 187 NotifyListeners(&event); |
| 188 return true; |
| 189 } |
| 190 |
| 191 bool AuthWatcher::AuthenticateLocally(string email) { |
| 192 user_settings_->GetEmailForSignin(&email); |
| 193 if (file_util::PathExists(dirman_->GetSyncDataDatabasePath())) { |
| 194 gaia_->SetUsername(email); |
| 195 status_ = LOCALLY_AUTHENTICATED; |
| 196 user_settings_->SwitchUser(email); |
| 197 PathString share_name; |
| 198 CHECK(AppendUTF8ToPathString(email.data(), email.size(), &share_name)); |
| 199 LoadDirectoryListAndOpen(share_name); |
| 200 NotifyAuthSucceeded(email); |
| 201 return true; |
| 202 } else { |
| 203 return false; |
| 204 } |
| 205 } |
| 206 |
| 207 bool AuthWatcher::AuthenticateLocally(string email, const string& password) { |
| 208 user_settings_->GetEmailForSignin(&email); |
| 209 return user_settings_->VerifyAgainstStoredHash(email, password) |
| 210 && AuthenticateLocally(email); |
| 211 } |
| 212 |
| 213 void AuthWatcher::ProcessGaiaAuthFailure() { |
| 214 GaiaAuthenticator::AuthResults results = gaia_->results(); |
| 215 if (LOCALLY_AUTHENTICATED == status_) { |
| 216 return; // nothing todo |
| 217 } else if (AuthenticateLocally(results.email, results.password)) { |
| 218 // We save the "Remember me" checkbox by putting a non-null auth |
| 219 // token into the last_user table. So if we're offline and the |
| 220 // user checks the box, insert a bogus auth token. |
| 221 if (PERSIST_TO_DISK == results.credentials_saved) { |
| 222 const string auth_token("bogus"); |
| 223 user_settings_->SetAuthTokenForService(results.email, |
| 224 SYNC_SERVICE_NAME, |
| 225 auth_token); |
| 226 } |
| 227 const bool unavailable = ConnectionUnavailable == results.auth_error || |
| 228 Unknown == results.auth_error || |
| 229 ServiceUnavailable == results.auth_error; |
| 230 if (unavailable) |
| 231 return; |
| 232 } |
| 233 AuthWatcherEvent myevent = { AuthWatcherEvent::GAIA_AUTH_FAILED, &results }; |
| 234 NotifyListeners(&myevent); |
| 235 } |
| 236 |
| 237 void* AuthWatcher::AuthenticationThreadMain(ThreadParams* args) { |
| 238 NameCurrentThreadForDebugging("SyncEngine_AuthWatcherThread"); |
| 239 { |
| 240 // This short lock ensures our launching function (StartNewAuthAttempt) is |
| 241 // done. |
| 242 MutexLock lock(&mutex_); |
| 243 current_attempt_trigger_ = args->trigger; |
| 244 } |
| 245 SaveCredentials save = args->persist_creds_to_disk ? |
| 246 PERSIST_TO_DISK : SAVE_IN_MEMORY_ONLY; |
| 247 int attempt = 0; |
| 248 SignIn const signin = user_settings_-> |
| 249 RecallSigninType(args->email, GMAIL_SIGNIN); |
| 250 |
| 251 if (!args->password.empty()) while (true) { |
| 252 bool authenticated; |
| 253 if (!args->captcha_token.empty() && !args->captcha_value.empty()) |
| 254 authenticated = gaia_->Authenticate(args->email, args->password, |
| 255 save, true, args->captcha_token, |
| 256 args->captcha_value, signin); |
| 257 else |
| 258 authenticated = gaia_->Authenticate(args->email, args->password, |
| 259 save, true, signin); |
| 260 if (authenticated) { |
| 261 if (!ProcessGaiaAuthSuccess()) { |
| 262 if (3 != ++attempt) |
| 263 continue; |
| 264 AuthWatcherEvent event = |
| 265 { AuthWatcherEvent::SERVICE_CONNECTION_FAILED, 0 }; |
| 266 NotifyListeners(&event); |
| 267 } |
| 268 } else { |
| 269 ProcessGaiaAuthFailure(); |
| 270 } |
| 271 break; |
| 272 } else if (!args->auth_token.empty()) { |
| 273 AuthenticateWithToken(args->email, args->auth_token); |
| 274 } else { |
| 275 LOG(ERROR) << "Attempt to authenticate with no credentials."; |
| 276 } |
| 277 { |
| 278 MutexLock lock(&mutex_); |
| 279 authenticating_now_ = false; |
| 280 } |
| 281 delete args; |
| 282 return 0; |
| 283 } |
| 284 |
| 285 void AuthWatcher::Reset() { |
| 286 status_ = NOT_AUTHENTICATED; |
| 287 } |
| 288 |
| 289 void AuthWatcher::NotifyAuthSucceeded(const string& email) { |
| 290 LOG(INFO) << "NotifyAuthSucceeded"; |
| 291 AuthWatcherEvent event = { AuthWatcherEvent::AUTH_SUCCEEDED }; |
| 292 event.user_email = email; |
| 293 |
| 294 NotifyListeners(&event); |
| 295 } |
| 296 |
| 297 bool AuthWatcher::StartNewAuthAttempt(const string& email, |
| 298 const string& password, const string& auth_token, |
| 299 const string& captcha_token, const string& captcha_value, |
| 300 bool persist_creds_to_disk, |
| 301 AuthWatcherEvent::AuthenticationTrigger trigger) { |
| 302 AuthWatcherEvent event = { AuthWatcherEvent::AUTHENTICATION_ATTEMPT_START }; |
| 303 NotifyListeners(&event); |
| 304 MutexLock lock(&mutex_); |
| 305 if (authenticating_now_) |
| 306 return false; |
| 307 if (thread_handle_valid_) { |
| 308 int join_return = pthread_join(thread_, 0); |
| 309 if (0 != join_return) |
| 310 LOG(ERROR) << "pthread_join failed returning " << join_return; |
| 311 } |
| 312 string mail = email; |
| 313 if (email.find('@') == string::npos) { |
| 314 mail.push_back('@'); |
| 315 // TODO(chron): Should this be done only at the UI level? |
| 316 mail.append(DEFAULT_SIGNIN_DOMAIN); |
| 317 } |
| 318 ThreadParams* args = new ThreadParams; |
| 319 args->self = this; |
| 320 args->email = mail; |
| 321 args->password = password; |
| 322 args->auth_token = auth_token; |
| 323 args->captcha_token = captcha_token; |
| 324 args->captcha_value = captcha_value; |
| 325 args->persist_creds_to_disk = persist_creds_to_disk; |
| 326 args->trigger = trigger; |
| 327 if (0 != pthread_create(&thread_, NULL, AuthenticationThreadStartRoutine, |
| 328 args)) { |
| 329 LOG(ERROR) << "Failed to create auth thread."; |
| 330 return false; |
| 331 } |
| 332 authenticating_now_ = true; |
| 333 thread_handle_valid_ = true; |
| 334 return true; |
| 335 } |
| 336 |
| 337 void AuthWatcher::WaitForAuthThreadFinish() { |
| 338 { |
| 339 MutexLock lock(&mutex_); |
| 340 if (!thread_handle_valid_) |
| 341 return; |
| 342 } |
| 343 pthread_join(thread_, 0); |
| 344 } |
| 345 |
| 346 void AuthWatcher::HandleServerConnectionEvent( |
| 347 const ServerConnectionEvent& event) { |
| 348 if (event.server_reachable && |
| 349 !authenticating_now_ && |
| 350 (event.connection_code == HttpResponse::SYNC_AUTH_ERROR || |
| 351 status_ == LOCALLY_AUTHENTICATED)) { |
| 352 // We're either online or just got reconnected and want to try to |
| 353 // authenticate. If we've got a saved token this should just work. If not |
| 354 // the auth failure should trigger UI indications that we're not logged in. |
| 355 |
| 356 // METRIC: If we get a SYNC_AUTH_ERROR, our token expired. |
| 357 GaiaAuthenticator::AuthResults authresults = gaia_->results(); |
| 358 if (!StartNewAuthAttempt(authresults.email, authresults.password, |
| 359 authresults.auth_token, "", "", |
| 360 PERSIST_TO_DISK == authresults.credentials_saved, |
| 361 AuthWatcherEvent::EXPIRED_CREDENTIALS)) |
| 362 LOG(INFO) << "Couldn't start a new auth attempt."; |
| 363 } |
| 364 } |
| 365 |
| 366 bool AuthWatcher::LoadDirectoryListAndOpen(const PathString& login) { |
| 367 LOG(INFO) << "LoadDirectoryListAndOpen(" << login << ")"; |
| 368 bool initial_sync_ended = false; |
| 369 |
| 370 dirman_->Open(login); |
| 371 syncable::ScopedDirLookup dir(dirman_, login); |
| 372 if (dir.good() && dir->initial_sync_ended()) |
| 373 initial_sync_ended = true; |
| 374 |
| 375 LOG(INFO) << "LoadDirectoryListAndOpen returning " << initial_sync_ended; |
| 376 return initial_sync_ended; |
| 377 } |
| 378 |
| 379 AuthWatcher::~AuthWatcher() { |
| 380 WaitForAuthThreadFinish(); |
| 381 } |
| 382 |
| 383 void AuthWatcher::Authenticate(const string& email, const string& password, |
| 384 const string& captcha_token, const string& captcha_value, |
| 385 bool persist_creds_to_disk) { |
| 386 LOG(INFO) << "AuthWatcher::Authenticate called"; |
| 387 WaitForAuthThreadFinish(); |
| 388 |
| 389 // We CHECK here because WaitForAuthThreadFinish should ensure there's no |
| 390 // ongoing auth attempt. |
| 391 string empty; |
| 392 CHECK(StartNewAuthAttempt(email, password, empty, captcha_token, |
| 393 captcha_value, persist_creds_to_disk, |
| 394 AuthWatcherEvent::USER_INITIATED)); |
| 395 } |
| 396 |
| 397 void AuthWatcher::Logout() { |
| 398 scm_->ResetAuthStatus(); |
| 399 Reset(); |
| 400 WaitForAuthThreadFinish(); |
| 401 ClearAuthenticationData(); |
| 402 } |
| 403 |
| 404 void AuthWatcher::ClearAuthenticationData() { |
| 405 sync_service_token_.clear(); |
| 406 scm_->set_auth_token(sync_service_token()); |
| 407 user_settings_->ClearAllServiceTokens(); |
| 408 } |
| 409 |
| 410 string AuthWatcher::email() const { |
| 411 return gaia_->email(); |
| 412 } |
| 413 |
| 414 void AuthWatcher::NotifyListeners(AuthWatcherEvent* event) { |
| 415 event->trigger = current_attempt_trigger_; |
| 416 channel_->NotifyListeners(*event); |
| 417 } |
| 418 |
| 419 } // namespace browser_sync |
OLD | NEW |