blob: 57b10f8d5440e354b5d7fb7e2a35beacf6f49611 [file] [log] [blame]
// 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 "components/signin/ios/browser/profile_oauth2_token_service_ios_delegate.h"
#import <Foundation/Foundation.h>
#include <memory>
#include <set>
#include <string>
#include <vector>
#include "base/bind.h"
#include "base/macros.h"
#include "base/message_loop/message_loop.h"
#include "base/stl_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/values.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/signin/core/browser/account_info.h"
#include "components/signin/core/browser/account_tracker_service.h"
#include "components/signin/core/browser/signin_client.h"
#include "components/signin/core/common/signin_pref_names.h"
#include "components/signin/ios/browser/profile_oauth2_token_service_ios_provider.h"
#include "google_apis/gaia/oauth2_access_token_fetcher.h"
#include "net/url_request/url_request_status.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Match the way Chromium handles authentication errors in
// google_apis/gaia/oauth2_access_token_fetcher.cc:
GoogleServiceAuthError GetGoogleServiceAuthErrorFromNSError(
ProfileOAuth2TokenServiceIOSProvider* provider,
const std::string& gaia_id,
NSError* error) {
if (!error)
return GoogleServiceAuthError::AuthErrorNone();
AuthenticationErrorCategory errorCategory =
provider->GetAuthenticationErrorCategory(gaia_id, error);
switch (errorCategory) {
case kAuthenticationErrorCategoryUnknownErrors:
// Treat all unknown error as unexpected service response errors.
// This may be too general and may require a finer grain filtering.
return GoogleServiceAuthError(
GoogleServiceAuthError::UNEXPECTED_SERVICE_RESPONSE);
case kAuthenticationErrorCategoryAuthorizationErrors:
return GoogleServiceAuthError(
GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS);
case kAuthenticationErrorCategoryAuthorizationForbiddenErrors:
// HTTP_FORBIDDEN (403) is treated as temporary error, because it may be
// '403 Rate Limit Exceeded.' (for more details, see
// google_apis/gaia/oauth2_access_token_fetcher.cc).
return GoogleServiceAuthError(
GoogleServiceAuthError::SERVICE_UNAVAILABLE);
case kAuthenticationErrorCategoryNetworkServerErrors:
// Just set the connection error state to FAILED.
return GoogleServiceAuthError::FromConnectionError(
net::URLRequestStatus::FAILED);
case kAuthenticationErrorCategoryUserCancellationErrors:
return GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED);
case kAuthenticationErrorCategoryUnknownIdentityErrors:
return GoogleServiceAuthError(GoogleServiceAuthError::USER_NOT_SIGNED_UP);
}
}
class SSOAccessTokenFetcher : public OAuth2AccessTokenFetcher {
public:
SSOAccessTokenFetcher(OAuth2AccessTokenConsumer* consumer,
ProfileOAuth2TokenServiceIOSProvider* provider,
const AccountInfo& account);
~SSOAccessTokenFetcher() override;
void Start(const std::string& client_id,
const std::string& client_secret,
const std::vector<std::string>& scopes) override;
void CancelRequest() override;
// Handles an access token response.
void OnAccessTokenResponse(NSString* token,
NSDate* expiration,
NSError* error);
private:
ProfileOAuth2TokenServiceIOSProvider* provider_; // weak
AccountInfo account_;
bool request_was_cancelled_;
base::WeakPtrFactory<SSOAccessTokenFetcher> weak_factory_;
DISALLOW_COPY_AND_ASSIGN(SSOAccessTokenFetcher);
};
SSOAccessTokenFetcher::SSOAccessTokenFetcher(
OAuth2AccessTokenConsumer* consumer,
ProfileOAuth2TokenServiceIOSProvider* provider,
const AccountInfo& account)
: OAuth2AccessTokenFetcher(consumer),
provider_(provider),
account_(account),
request_was_cancelled_(false),
weak_factory_(this) {
DCHECK(provider_);
}
SSOAccessTokenFetcher::~SSOAccessTokenFetcher() {
}
void SSOAccessTokenFetcher::Start(const std::string& client_id,
const std::string& client_secret,
const std::vector<std::string>& scopes) {
std::set<std::string> scopes_set(scopes.begin(), scopes.end());
provider_->GetAccessToken(
account_.gaia, client_id, client_secret, scopes_set,
base::Bind(&SSOAccessTokenFetcher::OnAccessTokenResponse,
weak_factory_.GetWeakPtr()));
}
void SSOAccessTokenFetcher::CancelRequest() {
request_was_cancelled_ = true;
}
void SSOAccessTokenFetcher::OnAccessTokenResponse(NSString* token,
NSDate* expiration,
NSError* error) {
if (request_was_cancelled_) {
// Ignore the callback if the request was cancelled.
return;
}
GoogleServiceAuthError auth_error =
GetGoogleServiceAuthErrorFromNSError(provider_, account_.gaia, error);
if (auth_error.state() == GoogleServiceAuthError::NONE) {
base::Time expiration_date =
base::Time::FromDoubleT([expiration timeIntervalSince1970]);
FireOnGetTokenSuccess(base::SysNSStringToUTF8(token), expiration_date);
} else {
FireOnGetTokenFailure(auth_error);
}
}
} // namespace
ProfileOAuth2TokenServiceIOSDelegate::AccountStatus::AccountStatus(
SigninErrorController* signin_error_controller,
const std::string& account_id)
: signin_error_controller_(signin_error_controller),
account_id_(account_id),
last_auth_error_(GoogleServiceAuthError::NONE) {
DCHECK(signin_error_controller_);
DCHECK(!account_id_.empty());
signin_error_controller_->AddProvider(this);
}
ProfileOAuth2TokenServiceIOSDelegate::AccountStatus::~AccountStatus() {
signin_error_controller_->RemoveProvider(this);
}
void ProfileOAuth2TokenServiceIOSDelegate::AccountStatus::SetLastAuthError(
const GoogleServiceAuthError& error) {
if (error.state() != last_auth_error_.state()) {
last_auth_error_ = error;
signin_error_controller_->AuthStatusChanged();
}
}
std::string ProfileOAuth2TokenServiceIOSDelegate::AccountStatus::GetAccountId()
const {
return account_id_;
}
GoogleServiceAuthError
ProfileOAuth2TokenServiceIOSDelegate::AccountStatus::GetAuthStatus() const {
return last_auth_error_;
}
ProfileOAuth2TokenServiceIOSDelegate::ProfileOAuth2TokenServiceIOSDelegate(
SigninClient* client,
std::unique_ptr<ProfileOAuth2TokenServiceIOSProvider> provider,
AccountTrackerService* account_tracker_service,
SigninErrorController* signin_error_controller)
: client_(client),
provider_(std::move(provider)),
account_tracker_service_(account_tracker_service),
signin_error_controller_(signin_error_controller) {
DCHECK(client_);
DCHECK(provider_);
DCHECK(account_tracker_service_);
DCHECK(signin_error_controller_);
}
ProfileOAuth2TokenServiceIOSDelegate::~ProfileOAuth2TokenServiceIOSDelegate() {
DCHECK(thread_checker_.CalledOnValidThread());
}
void ProfileOAuth2TokenServiceIOSDelegate::Shutdown() {
DCHECK(thread_checker_.CalledOnValidThread());
accounts_.clear();
}
void ProfileOAuth2TokenServiceIOSDelegate::LoadCredentials(
const std::string& primary_account_id) {
DCHECK(thread_checker_.CalledOnValidThread());
// Clean-up stale data from prefs.
ClearExcludedSecondaryAccounts();
if (primary_account_id.empty()) {
// On startup, always fire refresh token loaded even if there is nothing
// to load (not authenticated).
FireRefreshTokensLoaded();
return;
}
ReloadCredentials(primary_account_id);
FireRefreshTokensLoaded();
}
void ProfileOAuth2TokenServiceIOSDelegate::ReloadCredentials(
const std::string& primary_account_id) {
DCHECK(!primary_account_id.empty());
DCHECK(primary_account_id_.empty() ||
primary_account_id_ == primary_account_id);
primary_account_id_ = primary_account_id;
ReloadCredentials();
}
void ProfileOAuth2TokenServiceIOSDelegate::ReloadCredentials() {
DCHECK(thread_checker_.CalledOnValidThread());
if (primary_account_id_.empty()) {
// Avoid loading the credentials if there is no primary account id.
return;
}
// Get the list of new account ids.
std::set<std::string> new_account_ids;
for (const auto& new_account : provider_->GetAllAccounts()) {
DCHECK(!new_account.gaia.empty());
DCHECK(!new_account.email.empty());
// Account must to be seeded before adding an account to ensure that
// the GAIA ID is available if any client of this token service starts
// a fetch access token operation when it receives a
// |OnRefreshTokenAvailable| notification.
std::string account_id = account_tracker_service_->SeedAccountInfo(
new_account.gaia, new_account.email);
new_account_ids.insert(account_id);
}
// Get the list of existing account ids.
std::vector<std::string> old_account_ids = GetAccounts();
std::sort(old_account_ids.begin(), old_account_ids.end());
std::set<std::string> accounts_to_add =
base::STLSetDifference<std::set<std::string>>(new_account_ids,
old_account_ids);
std::set<std::string> accounts_to_remove =
base::STLSetDifference<std::set<std::string>>(old_account_ids,
new_account_ids);
if (accounts_to_add.empty() && accounts_to_remove.empty())
return;
// Remove all old accounts that do not appear in |new_accounts| and then
// load |new_accounts|.
ScopedBatchChange batch(this);
for (const auto& account_to_remove : accounts_to_remove) {
RemoveAccount(account_to_remove);
}
// Load all new_accounts.
for (const auto& account_to_add : accounts_to_add) {
AddOrUpdateAccount(account_to_add);
}
}
void ProfileOAuth2TokenServiceIOSDelegate::UpdateCredentials(
const std::string& account_id,
const std::string& refresh_token) {
DCHECK(thread_checker_.CalledOnValidThread());
NOTREACHED() << "Unexpected call to UpdateCredentials when using shared "
"authentication.";
}
void ProfileOAuth2TokenServiceIOSDelegate::RevokeAllCredentials() {
DCHECK(thread_checker_.CalledOnValidThread());
ScopedBatchChange batch(this);
AccountStatusMap toRemove = accounts_;
for (auto& accountStatus : toRemove)
RemoveAccount(accountStatus.first);
DCHECK_EQ(0u, accounts_.size());
primary_account_id_.clear();
ClearExcludedSecondaryAccounts();
}
OAuth2AccessTokenFetcher*
ProfileOAuth2TokenServiceIOSDelegate::CreateAccessTokenFetcher(
const std::string& account_id,
net::URLRequestContextGetter* getter,
OAuth2AccessTokenConsumer* consumer) {
AccountInfo account_info =
account_tracker_service_->GetAccountInfo(account_id);
return new SSOAccessTokenFetcher(consumer, provider_.get(), account_info);
}
std::vector<std::string> ProfileOAuth2TokenServiceIOSDelegate::GetAccounts() {
DCHECK(thread_checker_.CalledOnValidThread());
std::vector<std::string> account_ids;
for (auto i = accounts_.begin(); i != accounts_.end(); ++i)
account_ids.push_back(i->first);
return account_ids;
}
bool ProfileOAuth2TokenServiceIOSDelegate::RefreshTokenIsAvailable(
const std::string& account_id) const {
DCHECK(thread_checker_.CalledOnValidThread());
return accounts_.count(account_id) > 0;
}
bool ProfileOAuth2TokenServiceIOSDelegate::RefreshTokenHasError(
const std::string& account_id) const {
DCHECK(thread_checker_.CalledOnValidThread());
auto it = accounts_.find(account_id);
// TODO(rogerta): should we distinguish between transient and persistent?
return it == accounts_.end() ? false : IsError(it->second->GetAuthStatus());
}
void ProfileOAuth2TokenServiceIOSDelegate::UpdateAuthError(
const std::string& account_id,
const GoogleServiceAuthError& error) {
DCHECK(thread_checker_.CalledOnValidThread());
// 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.
if (error.state() == GoogleServiceAuthError::CONNECTION_FAILED ||
error.state() == GoogleServiceAuthError::SERVICE_UNAVAILABLE) {
return;
}
if (accounts_.count(account_id) == 0) {
// Nothing to update as the account has already been removed.
return;
}
accounts_[account_id]->SetLastAuthError(error);
}
// Clear the authentication error state and notify all observers that a new
// refresh token is available so that they request new access tokens.
void ProfileOAuth2TokenServiceIOSDelegate::AddOrUpdateAccount(
const std::string& account_id) {
DCHECK(thread_checker_.CalledOnValidThread());
// Account must have been seeded before attempting to add it.
DCHECK(!account_tracker_service_->GetAccountInfo(account_id).gaia.empty());
DCHECK(!account_tracker_service_->GetAccountInfo(account_id).email.empty());
bool account_present = accounts_.count(account_id) > 0;
if (account_present &&
accounts_[account_id]->GetAuthStatus().state() ==
GoogleServiceAuthError::NONE) {
// No need to update the account if it is already a known account and if
// there is no auth error.
return;
}
if (!account_present) {
accounts_[account_id].reset(
new AccountStatus(signin_error_controller_, account_id));
}
UpdateAuthError(account_id, GoogleServiceAuthError::AuthErrorNone());
FireRefreshTokenAvailable(account_id);
}
void ProfileOAuth2TokenServiceIOSDelegate::RemoveAccount(
const std::string& account_id) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(!account_id.empty());
if (accounts_.count(account_id) > 0) {
accounts_.erase(account_id);
FireRefreshTokenRevoked(account_id);
}
}
void ProfileOAuth2TokenServiceIOSDelegate::ClearExcludedSecondaryAccounts() {
client_->GetPrefs()->ClearPref(
prefs::kTokenServiceExcludeAllSecondaryAccounts);
client_->GetPrefs()->ClearPref(prefs::kTokenServiceExcludedSecondaryAccounts);
}