// Copyright 2015 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/signin/mutable_profile_oauth2_token_service_delegate.h" #include "base/profiler/scoped_tracker.h" #include "components/signin/core/browser/signin_client.h" #include "components/signin/core/browser/signin_metrics.h" #include "components/signin/core/browser/webdata/token_web_data.h" #include "components/webdata/common/web_data_service_base.h" #include "google_apis/gaia/gaia_auth_fetcher.h" #include "google_apis/gaia/gaia_auth_util.h" #include "google_apis/gaia/gaia_constants.h" #include "google_apis/gaia/oauth2_access_token_fetcher_immediate_error.h" #include "google_apis/gaia/oauth2_access_token_fetcher_impl.h" #include "net/url_request/url_request_context_getter.h" namespace { const char kAccountIdPrefix[] = "AccountId-"; const size_t kAccountIdPrefixLength = 10; std::string ApplyAccountIdPrefix(const std::string& account_id) { return kAccountIdPrefix + account_id; } bool IsLegacyRefreshTokenId(const std::string& service_id) { return service_id == GaiaConstants::kGaiaOAuth2LoginRefreshToken; } bool IsLegacyServiceId(const std::string& account_id) { return account_id.compare(0u, kAccountIdPrefixLength, kAccountIdPrefix) != 0; } std::string RemoveAccountIdPrefix(const std::string& prefixed_account_id) { return prefixed_account_id.substr(kAccountIdPrefixLength); } } // namespace // This class sends a request to GAIA to revoke the given refresh token from // the server. This is a best effort attempt only. This class deletes itself // when done sucessfully or otherwise. class MutableProfileOAuth2TokenServiceDelegate::RevokeServerRefreshToken : public GaiaAuthConsumer { public: RevokeServerRefreshToken( MutableProfileOAuth2TokenServiceDelegate* token_service_delegate, const std::string& account_id); ~RevokeServerRefreshToken() override; private: // GaiaAuthConsumer overrides: void OnOAuth2RevokeTokenCompleted() override; MutableProfileOAuth2TokenServiceDelegate* token_service_delegate_; GaiaAuthFetcher fetcher_; DISALLOW_COPY_AND_ASSIGN(RevokeServerRefreshToken); }; MutableProfileOAuth2TokenServiceDelegate::RevokeServerRefreshToken:: RevokeServerRefreshToken( MutableProfileOAuth2TokenServiceDelegate* token_service_delegate, const std::string& refresh_token) : token_service_delegate_(token_service_delegate), fetcher_(this, GaiaConstants::kChromeSource, token_service_delegate_->GetRequestContext()) { fetcher_.StartRevokeOAuth2Token(refresh_token); } MutableProfileOAuth2TokenServiceDelegate::RevokeServerRefreshToken:: ~RevokeServerRefreshToken() { } void MutableProfileOAuth2TokenServiceDelegate::RevokeServerRefreshToken:: OnOAuth2RevokeTokenCompleted() { // |this| pointer will be deleted when removed from the vector, so don't // access any members after call to erase(). token_service_delegate_->server_revokes_.erase( std::find(token_service_delegate_->server_revokes_.begin(), token_service_delegate_->server_revokes_.end(), this)); } MutableProfileOAuth2TokenServiceDelegate::AccountInfo::AccountInfo( SigninErrorController* signin_error_controller, const std::string& account_id, const std::string& refresh_token) : signin_error_controller_(signin_error_controller), account_id_(account_id), refresh_token_(refresh_token), last_auth_error_(GoogleServiceAuthError::NONE) { DCHECK(signin_error_controller_); DCHECK(!account_id_.empty()); signin_error_controller_->AddProvider(this); } MutableProfileOAuth2TokenServiceDelegate::AccountInfo::~AccountInfo() { signin_error_controller_->RemoveProvider(this); } void MutableProfileOAuth2TokenServiceDelegate::AccountInfo::SetLastAuthError( const GoogleServiceAuthError& error) { if (error.state() != last_auth_error_.state()) { last_auth_error_ = error; signin_error_controller_->AuthStatusChanged(); } } std::string MutableProfileOAuth2TokenServiceDelegate::AccountInfo::GetAccountId() const { return account_id_; } GoogleServiceAuthError MutableProfileOAuth2TokenServiceDelegate::AccountInfo::GetAuthStatus() const { return last_auth_error_; } MutableProfileOAuth2TokenServiceDelegate:: MutableProfileOAuth2TokenServiceDelegate( SigninClient* client, SigninErrorController* signin_error_controller) : web_data_service_request_(0), backoff_entry_(&backoff_policy_), backoff_error_(GoogleServiceAuthError::NONE), client_(client), signin_error_controller_(signin_error_controller) { VLOG(1) << "MutablePO2TS::MutablePO2TS"; DCHECK(client); DCHECK(signin_error_controller); // It's okay to fill the backoff policy after being used in construction. backoff_policy_.num_errors_to_ignore = 0; backoff_policy_.initial_delay_ms = 1000; backoff_policy_.multiply_factor = 2.0; backoff_policy_.jitter_factor = 0.2; backoff_policy_.maximum_backoff_ms = 15 * 60 * 1000; backoff_policy_.entry_lifetime_ms = -1; backoff_policy_.always_use_initial_delay = false; } MutableProfileOAuth2TokenServiceDelegate:: ~MutableProfileOAuth2TokenServiceDelegate() { VLOG(1) << "MutablePO2TS::~MutablePO2TS"; DCHECK(server_revokes_.empty()); } OAuth2AccessTokenFetcher* MutableProfileOAuth2TokenServiceDelegate::CreateAccessTokenFetcher( const std::string& account_id, net::URLRequestContextGetter* getter, OAuth2AccessTokenConsumer* consumer) { ValidateAccountId(account_id); // check whether the account has persistent error. if (refresh_tokens_[account_id]->GetAuthStatus().IsPersistentError()) { VLOG(1) << "Request for token has been rejected due to persistent error #" << refresh_tokens_[account_id]->GetAuthStatus().state(); return new OAuth2AccessTokenFetcherImmediateError( consumer, refresh_tokens_[account_id]->GetAuthStatus()); } if (backoff_entry_.ShouldRejectRequest()) { VLOG(1) << "Request for token has been rejected due to backoff rules from" << " previous error #" << backoff_error_.state(); return new OAuth2AccessTokenFetcherImmediateError(consumer, backoff_error_); } std::string refresh_token = GetRefreshToken(account_id); DCHECK(!refresh_token.empty()); return new OAuth2AccessTokenFetcherImpl(consumer, getter, refresh_token); } void MutableProfileOAuth2TokenServiceDelegate::UpdateAuthError( const std::string& account_id, const GoogleServiceAuthError& error) { VLOG(1) << "MutablePO2TS::UpdateAuthError. Error: " << error.state(); backoff_entry_.InformOfRequest(!error.IsTransientError()); ValidateAccountId(account_id); // Do not report connection errors as these are not actually auth errors. // We also want to avoid masking a "real" auth error just because we // subsequently get a transient network error. We do keep it around though // to report for future requests being denied for "backoff" reasons. if (error.IsTransientError()) { backoff_error_ = error; return; } if (refresh_tokens_.count(account_id) == 0) { // This could happen if the preferences have been corrupted (see // http://crbug.com/321370). In a Debug build that would be a bug, but in a // Release build we want to deal with it gracefully. NOTREACHED(); return; } refresh_tokens_[account_id]->SetLastAuthError(error); } bool MutableProfileOAuth2TokenServiceDelegate::RefreshTokenIsAvailable( const std::string& account_id) const { VLOG(1) << "MutablePO2TS::RefreshTokenIsAvailable"; return !GetRefreshToken(account_id).empty(); } std::string MutableProfileOAuth2TokenServiceDelegate::GetRefreshToken( const std::string& account_id) const { AccountInfoMap::const_iterator iter = refresh_tokens_.find(account_id); if (iter != refresh_tokens_.end()) return iter->second->refresh_token(); return std::string(); } std::vector MutableProfileOAuth2TokenServiceDelegate::GetAccounts() { std::vector account_ids; for (AccountInfoMap::const_iterator iter = refresh_tokens_.begin(); iter != refresh_tokens_.end(); ++iter) { account_ids.push_back(iter->first); } return account_ids; } net::URLRequestContextGetter* MutableProfileOAuth2TokenServiceDelegate::GetRequestContext() const { return client_->GetURLRequestContext(); } void MutableProfileOAuth2TokenServiceDelegate::LoadCredentials( const std::string& primary_account_id) { DCHECK(!primary_account_id.empty()); ValidateAccountId(primary_account_id); DCHECK(loading_primary_account_id_.empty()); DCHECK_EQ(0, web_data_service_request_); refresh_tokens_.clear(); // If the account_id is an email address, then canonicalize it. This // is to support legacy account_ids, and will not be needed after // switching to gaia-ids. if (primary_account_id.find('@') != std::string::npos) { loading_primary_account_id_ = gaia::CanonicalizeEmail(primary_account_id); } else { loading_primary_account_id_ = primary_account_id; } scoped_refptr token_web_data = client_->GetDatabase(); if (token_web_data.get()) web_data_service_request_ = token_web_data->GetAllTokens(this); } void MutableProfileOAuth2TokenServiceDelegate::OnWebDataServiceRequestDone( WebDataServiceBase::Handle handle, const WDTypedResult* result) { VLOG(1) << "MutablePO2TS::OnWebDataServiceRequestDone. Result type: " << (result == nullptr ? -1 : (int)result->GetType()); // TODO(robliao): Remove ScopedTracker below once https://crbug.com/422460 is // fixed. tracked_objects::ScopedTracker tracking_profile( FROM_HERE_WITH_EXPLICIT_FUNCTION( "422460 MutableProfileOAuth2Token...::OnWebDataServiceRequestDone")); DCHECK_EQ(web_data_service_request_, handle); web_data_service_request_ = 0; if (result) { DCHECK(result->GetType() == TOKEN_RESULT); const WDResult>* token_result = static_cast>*>( result); LoadAllCredentialsIntoMemory(token_result->GetValue()); } // Make sure that we have an entry for |loading_primary_account_id_| in the // map. The entry could be missing if there is a corruption in the token DB // while this profile is connected to an account. DCHECK(!loading_primary_account_id_.empty()); if (refresh_tokens_.count(loading_primary_account_id_) == 0) { refresh_tokens_[loading_primary_account_id_].reset(new AccountInfo( signin_error_controller_, loading_primary_account_id_, std::string())); } // If we don't have a refresh token for a known account, signal an error. for (AccountInfoMap::const_iterator i = refresh_tokens_.begin(); i != refresh_tokens_.end(); ++i) { if (!RefreshTokenIsAvailable(i->first)) { UpdateAuthError(i->first, GoogleServiceAuthError( GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); break; } } loading_primary_account_id_.clear(); } void MutableProfileOAuth2TokenServiceDelegate::LoadAllCredentialsIntoMemory( const std::map& db_tokens) { std::string old_login_token; { ScopedBatchChange batch(this); VLOG(1) << "MutablePO2TS::LoadAllCredentialsIntoMemory; " << db_tokens.size() << " Credential(s)."; for (std::map::const_iterator iter = db_tokens.begin(); iter != db_tokens.end(); ++iter) { std::string prefixed_account_id = iter->first; std::string refresh_token = iter->second; if (IsLegacyRefreshTokenId(prefixed_account_id) && !refresh_token.empty()) old_login_token = refresh_token; if (IsLegacyServiceId(prefixed_account_id)) { scoped_refptr token_web_data = client_->GetDatabase(); if (token_web_data.get()) token_web_data->RemoveTokenForService(prefixed_account_id); } else { DCHECK(!refresh_token.empty()); std::string account_id = RemoveAccountIdPrefix(prefixed_account_id); // If the account_id is an email address, then canonicalize it. This // is to support legacy account_ids, and will not be needed after // switching to gaia-ids. if (account_id.find('@') != std::string::npos) { // If the canonical account id is not the same as the loaded // account id, make sure not to overwrite a refresh token from // a canonical version. If no canonical version was loaded, then // re-persist this refresh token with the canonical account id. std::string canon_account_id = gaia::CanonicalizeEmail(account_id); if (canon_account_id != account_id) { ClearPersistedCredentials(account_id); if (db_tokens.count(ApplyAccountIdPrefix(canon_account_id)) == 0) PersistCredentials(canon_account_id, refresh_token); } account_id = canon_account_id; } refresh_tokens_[account_id].reset(new AccountInfo( signin_error_controller_, account_id, refresh_token)); FireRefreshTokenAvailable(account_id); } } if (!old_login_token.empty()) { DCHECK(!loading_primary_account_id_.empty()); if (refresh_tokens_.count(loading_primary_account_id_) == 0) UpdateCredentials(loading_primary_account_id_, old_login_token); } } FireRefreshTokensLoaded(); } void MutableProfileOAuth2TokenServiceDelegate::UpdateCredentials( const std::string& account_id, const std::string& refresh_token) { DCHECK(thread_checker_.CalledOnValidThread()); DCHECK(!account_id.empty()); DCHECK(!refresh_token.empty()); ValidateAccountId(account_id); signin_metrics::LogSigninAddAccount(); bool refresh_token_present = refresh_tokens_.count(account_id) > 0; if (!refresh_token_present || refresh_tokens_[account_id]->refresh_token() != refresh_token) { ScopedBatchChange batch(this); // If token present, and different from the new one, cancel its requests, // and clear the entries in cache related to that account. if (refresh_token_present) { VLOG(1) << "MutablePO2TS::UpdateCredentials; Refresh Token was present. " << "account_id=" << account_id; refresh_tokens_[account_id]->set_refresh_token(refresh_token); } else { VLOG(1) << "MutablePO2TS::UpdateCredentials; Refresh Token was absent. " << "account_id=" << account_id; refresh_tokens_[account_id].reset( new AccountInfo(signin_error_controller_, account_id, refresh_token)); } // Save the token in memory and in persistent store. PersistCredentials(account_id, refresh_token); UpdateAuthError(account_id, GoogleServiceAuthError::AuthErrorNone()); FireRefreshTokenAvailable(account_id); } } void MutableProfileOAuth2TokenServiceDelegate::PersistCredentials( const std::string& account_id, const std::string& refresh_token) { scoped_refptr token_web_data = client_->GetDatabase(); if (token_web_data.get()) { VLOG(1) << "MutablePO2TS::PersistCredentials for account_id=" << account_id; token_web_data->SetTokenForService(ApplyAccountIdPrefix(account_id), refresh_token); } } void MutableProfileOAuth2TokenServiceDelegate::RevokeAllCredentials() { if (!client_->CanRevokeCredentials()) return; DCHECK(thread_checker_.CalledOnValidThread()); ScopedBatchChange batch(this); VLOG(1) << "MutablePO2TS::RevokeAllCredentials"; CancelWebTokenFetch(); AccountInfoMap tokens = refresh_tokens_; for (AccountInfoMap::iterator i = tokens.begin(); i != tokens.end(); ++i) RevokeCredentials(i->first); DCHECK_EQ(0u, refresh_tokens_.size()); // Make sure all tokens are removed. scoped_refptr token_web_data = client_->GetDatabase(); if (token_web_data.get()) token_web_data->RemoveAllTokens(); } void MutableProfileOAuth2TokenServiceDelegate::RevokeCredentials( const std::string& account_id) { ValidateAccountId(account_id); DCHECK(thread_checker_.CalledOnValidThread()); if (refresh_tokens_.count(account_id) > 0) { VLOG(1) << "MutablePO2TS::RevokeCredentials for account_id=" << account_id; ScopedBatchChange batch(this); RevokeCredentialsOnServer(refresh_tokens_[account_id]->refresh_token()); refresh_tokens_.erase(account_id); ClearPersistedCredentials(account_id); FireRefreshTokenRevoked(account_id); } } void MutableProfileOAuth2TokenServiceDelegate::ClearPersistedCredentials( const std::string& account_id) { scoped_refptr token_web_data = client_->GetDatabase(); if (token_web_data.get()) { VLOG(1) << "MutablePO2TS::ClearPersistedCredentials for account_id=" << account_id; token_web_data->RemoveTokenForService(ApplyAccountIdPrefix(account_id)); } } void MutableProfileOAuth2TokenServiceDelegate::RevokeCredentialsOnServer( const std::string& refresh_token) { // Keep track or all server revoke requests. This way they can be deleted // before the token service is shutdown and won't outlive the profile. server_revokes_.push_back(new RevokeServerRefreshToken(this, refresh_token)); } void MutableProfileOAuth2TokenServiceDelegate::CancelWebTokenFetch() { if (web_data_service_request_ != 0) { scoped_refptr token_web_data = client_->GetDatabase(); DCHECK(token_web_data.get()); token_web_data->CancelRequest(web_data_service_request_); web_data_service_request_ = 0; } } void MutableProfileOAuth2TokenServiceDelegate::Shutdown() { VLOG(1) << "MutablePO2TS::Shutdown"; server_revokes_.clear(); CancelWebTokenFetch(); refresh_tokens_.clear(); }