1 // Copyright 2015 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.
5 #include "components/signin/ios/browser/profile_oauth2_token_service_ios_delegate.h"
7 #include <Foundation/Foundation.h>
13 #include "base/bind.h"
14 #include "base/memory/scoped_ptr.h"
15 #include "base/message_loop/message_loop.h"
16 #include "base/prefs/pref_service.h"
17 #include "base/prefs/scoped_user_pref_update.h"
18 #include "base/stl_util.h"
19 #include "base/strings/sys_string_conversions.h"
20 #include "base/values.h"
21 #include "components/signin/core/browser/account_info.h"
22 #include "components/signin/core/browser/account_tracker_service.h"
23 #include "components/signin/core/browser/signin_client.h"
24 #include "components/signin/core/common/signin_pref_names.h"
25 #include "components/signin/ios/browser/profile_oauth2_token_service_ios_provider.h"
26 #include "google_apis/gaia/oauth2_access_token_fetcher.h"
27 #include "net/url_request/url_request_status.h"
31 // Match the way Chromium handles authentication errors in
32 // google_apis/gaia/oauth2_access_token_fetcher.cc:
33 GoogleServiceAuthError GetGoogleServiceAuthErrorFromNSError(
34 ProfileOAuth2TokenServiceIOSProvider* provider,
37 return GoogleServiceAuthError::AuthErrorNone();
39 AuthenticationErrorCategory errorCategory =
40 provider->GetAuthenticationErrorCategory(error);
41 switch (errorCategory) {
42 case kAuthenticationErrorCategoryUnknownErrors:
43 // Treat all unknown error as unexpected service response errors.
44 // This may be too general and may require a finer grain filtering.
45 return GoogleServiceAuthError(
46 GoogleServiceAuthError::UNEXPECTED_SERVICE_RESPONSE);
47 case kAuthenticationErrorCategoryAuthorizationErrors:
48 return GoogleServiceAuthError(
49 GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS);
50 case kAuthenticationErrorCategoryAuthorizationForbiddenErrors:
51 // HTTP_FORBIDDEN (403) is treated as temporary error, because it may be
52 // '403 Rate Limit Exceeded.' (for more details, see
53 // google_apis/gaia/oauth2_access_token_fetcher.cc).
54 return GoogleServiceAuthError(
55 GoogleServiceAuthError::SERVICE_UNAVAILABLE);
56 case kAuthenticationErrorCategoryNetworkServerErrors:
57 // Just set the connection error state to FAILED.
58 return GoogleServiceAuthError::FromConnectionError(
59 net::URLRequestStatus::FAILED);
60 case kAuthenticationErrorCategoryUserCancellationErrors:
61 return GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED);
62 case kAuthenticationErrorCategoryUnknownIdentityErrors:
63 return GoogleServiceAuthError(GoogleServiceAuthError::USER_NOT_SIGNED_UP);
67 class SSOAccessTokenFetcher : public OAuth2AccessTokenFetcher {
69 SSOAccessTokenFetcher(OAuth2AccessTokenConsumer* consumer,
70 ProfileOAuth2TokenServiceIOSProvider* provider,
71 const AccountInfo& account);
72 ~SSOAccessTokenFetcher() override;
74 void Start(const std::string& client_id,
75 const std::string& client_secret,
76 const std::vector<std::string>& scopes) override;
78 void CancelRequest() override;
80 // Handles an access token response.
81 void OnAccessTokenResponse(NSString* token,
86 ProfileOAuth2TokenServiceIOSProvider* provider_; // weak
88 bool request_was_cancelled_;
89 base::WeakPtrFactory<SSOAccessTokenFetcher> weak_factory_;
91 DISALLOW_COPY_AND_ASSIGN(SSOAccessTokenFetcher);
94 SSOAccessTokenFetcher::SSOAccessTokenFetcher(
95 OAuth2AccessTokenConsumer* consumer,
96 ProfileOAuth2TokenServiceIOSProvider* provider,
97 const AccountInfo& account)
98 : OAuth2AccessTokenFetcher(consumer),
101 request_was_cancelled_(false),
102 weak_factory_(this) {
106 SSOAccessTokenFetcher::~SSOAccessTokenFetcher() {
109 void SSOAccessTokenFetcher::Start(const std::string& client_id,
110 const std::string& client_secret,
111 const std::vector<std::string>& scopes) {
112 std::set<std::string> scopes_set(scopes.begin(), scopes.end());
113 provider_->GetAccessToken(
114 account_.gaia, client_id, client_secret, scopes_set,
115 base::Bind(&SSOAccessTokenFetcher::OnAccessTokenResponse,
116 weak_factory_.GetWeakPtr()));
119 void SSOAccessTokenFetcher::CancelRequest() {
120 request_was_cancelled_ = true;
123 void SSOAccessTokenFetcher::OnAccessTokenResponse(NSString* token,
126 if (request_was_cancelled_) {
127 // Ignore the callback if the request was cancelled.
130 GoogleServiceAuthError auth_error =
131 GetGoogleServiceAuthErrorFromNSError(provider_, error);
132 if (auth_error.state() == GoogleServiceAuthError::NONE) {
133 base::Time expiration_date =
134 base::Time::FromDoubleT([expiration timeIntervalSince1970]);
135 FireOnGetTokenSuccess(base::SysNSStringToUTF8(token), expiration_date);
137 FireOnGetTokenFailure(auth_error);
143 ProfileOAuth2TokenServiceIOSDelegate::AccountStatus::AccountStatus(
144 SigninErrorController* signin_error_controller,
145 const std::string& account_id)
146 : signin_error_controller_(signin_error_controller),
147 account_id_(account_id),
148 last_auth_error_(GoogleServiceAuthError::NONE) {
149 DCHECK(signin_error_controller_);
150 DCHECK(!account_id_.empty());
151 signin_error_controller_->AddProvider(this);
154 ProfileOAuth2TokenServiceIOSDelegate::AccountStatus::~AccountStatus() {
155 signin_error_controller_->RemoveProvider(this);
158 void ProfileOAuth2TokenServiceIOSDelegate::AccountStatus::SetLastAuthError(
159 const GoogleServiceAuthError& error) {
160 if (error.state() != last_auth_error_.state()) {
161 last_auth_error_ = error;
162 signin_error_controller_->AuthStatusChanged();
166 std::string ProfileOAuth2TokenServiceIOSDelegate::AccountStatus::GetAccountId()
171 GoogleServiceAuthError
172 ProfileOAuth2TokenServiceIOSDelegate::AccountStatus::GetAuthStatus() const {
173 return last_auth_error_;
176 ProfileOAuth2TokenServiceIOSDelegate::ProfileOAuth2TokenServiceIOSDelegate(
177 SigninClient* client,
178 ProfileOAuth2TokenServiceIOSProvider* provider,
179 AccountTrackerService* account_tracker_service,
180 SigninErrorController* signin_error_controller)
183 account_tracker_service_(account_tracker_service),
184 signin_error_controller_(signin_error_controller) {
187 DCHECK(account_tracker_service_);
188 DCHECK(signin_error_controller_);
191 ProfileOAuth2TokenServiceIOSDelegate::~ProfileOAuth2TokenServiceIOSDelegate() {
192 DCHECK(thread_checker_.CalledOnValidThread());
195 void ProfileOAuth2TokenServiceIOSDelegate::Shutdown() {
196 DCHECK(thread_checker_.CalledOnValidThread());
200 void ProfileOAuth2TokenServiceIOSDelegate::LoadCredentials(
201 const std::string& primary_account_id) {
202 DCHECK(thread_checker_.CalledOnValidThread());
203 if (account_tracker_service_->GetMigrationState() ==
204 AccountTrackerService::MIGRATION_IN_PROGRESS) {
205 MigrateExcludedSecondaryAccountIds();
208 // LoadCredentials() is called iff the user is signed in to Chrome, so the
209 // primary account id must not be empty.
210 DCHECK(!primary_account_id.empty());
212 ReloadCredentials(primary_account_id);
213 FireRefreshTokensLoaded();
216 void ProfileOAuth2TokenServiceIOSDelegate::ReloadCredentials(
217 const std::string& primary_account_id) {
218 DCHECK(!primary_account_id.empty());
219 DCHECK(primary_account_id_.empty() ||
220 primary_account_id_ == primary_account_id);
221 primary_account_id_ = primary_account_id;
225 void ProfileOAuth2TokenServiceIOSDelegate::ReloadCredentials() {
226 DCHECK(thread_checker_.CalledOnValidThread());
227 if (primary_account_id_.empty()) {
228 // Avoid loading the credentials if there is no primary account id.
232 // Get the list of new account ids.
233 std::set<std::string> excluded_account_ids = GetExcludedSecondaryAccounts();
234 std::set<std::string> new_account_ids;
235 for (const auto& new_account : provider_->GetAllAccounts()) {
236 DCHECK(!new_account.gaia.empty());
237 DCHECK(!new_account.email.empty());
238 if (!IsAccountExcluded(new_account.gaia, new_account.email,
239 excluded_account_ids)) {
240 // Account must to be seeded before adding an account to ensure that
241 // the GAIA ID is available if any client of this token service starts
242 // a fetch access token operation when it receives a
243 // |OnRefreshTokenAvailable| notification.
244 std::string account_id = account_tracker_service_->SeedAccountInfo(
245 new_account.gaia, new_account.email);
246 new_account_ids.insert(account_id);
250 // Get the list of existing account ids.
251 std::vector<std::string> old_account_ids = GetAccounts();
252 std::sort(old_account_ids.begin(), old_account_ids.end());
254 std::set<std::string> accounts_to_add =
255 base::STLSetDifference<std::set<std::string>>(new_account_ids,
257 std::set<std::string> accounts_to_remove =
258 base::STLSetDifference<std::set<std::string>>(old_account_ids,
260 if (accounts_to_add.empty() && accounts_to_remove.empty())
263 // Remove all old accounts that do not appear in |new_accounts| and then
264 // load |new_accounts|.
265 ScopedBatchChange batch(this);
266 for (const auto& account_to_remove : accounts_to_remove) {
267 RemoveAccount(account_to_remove);
270 // Load all new_accounts.
271 for (const auto& account_to_add : accounts_to_add) {
272 AddOrUpdateAccount(account_to_add);
276 void ProfileOAuth2TokenServiceIOSDelegate::UpdateCredentials(
277 const std::string& account_id,
278 const std::string& refresh_token) {
279 DCHECK(thread_checker_.CalledOnValidThread());
280 NOTREACHED() << "Unexpected call to UpdateCredentials when using shared "
284 void ProfileOAuth2TokenServiceIOSDelegate::RevokeAllCredentials() {
285 DCHECK(thread_checker_.CalledOnValidThread());
287 ScopedBatchChange batch(this);
288 AccountStatusMap toRemove = accounts_;
289 for (auto& accountStatus : toRemove)
290 RemoveAccount(accountStatus.first);
292 DCHECK_EQ(0u, accounts_.size());
293 primary_account_id_.clear();
294 ClearExcludedSecondaryAccounts();
297 OAuth2AccessTokenFetcher*
298 ProfileOAuth2TokenServiceIOSDelegate::CreateAccessTokenFetcher(
299 const std::string& account_id,
300 net::URLRequestContextGetter* getter,
301 OAuth2AccessTokenConsumer* consumer) {
302 AccountInfo account_info =
303 account_tracker_service_->GetAccountInfo(account_id);
304 return new SSOAccessTokenFetcher(consumer, provider_, account_info);
307 std::vector<std::string> ProfileOAuth2TokenServiceIOSDelegate::GetAccounts() {
308 DCHECK(thread_checker_.CalledOnValidThread());
309 std::vector<std::string> account_ids;
310 for (auto i = accounts_.begin(); i != accounts_.end(); ++i)
311 account_ids.push_back(i->first);
315 bool ProfileOAuth2TokenServiceIOSDelegate::RefreshTokenIsAvailable(
316 const std::string& account_id) const {
317 DCHECK(thread_checker_.CalledOnValidThread());
319 return accounts_.count(account_id) > 0;
322 bool ProfileOAuth2TokenServiceIOSDelegate::RefreshTokenHasError(
323 const std::string& account_id) const {
324 DCHECK(thread_checker_.CalledOnValidThread());
325 auto it = accounts_.find(account_id);
326 // TODO(rogerta): should we distinguish between transient and persistent?
327 return it == accounts_.end() ? false : IsError(it->second->GetAuthStatus());
330 void ProfileOAuth2TokenServiceIOSDelegate::UpdateAuthError(
331 const std::string& account_id,
332 const GoogleServiceAuthError& error) {
333 DCHECK(thread_checker_.CalledOnValidThread());
335 // Do not report connection errors as these are not actually auth errors.
336 // We also want to avoid masking a "real" auth error just because we
337 // subsequently get a transient network error.
338 if (error.state() == GoogleServiceAuthError::CONNECTION_FAILED ||
339 error.state() == GoogleServiceAuthError::SERVICE_UNAVAILABLE) {
343 if (accounts_.count(account_id) == 0) {
344 // Nothing to update as the account has already been removed.
347 accounts_[account_id]->SetLastAuthError(error);
350 // Clear the authentication error state and notify all observers that a new
351 // refresh token is available so that they request new access tokens.
352 void ProfileOAuth2TokenServiceIOSDelegate::AddOrUpdateAccount(
353 const std::string& account_id) {
354 DCHECK(thread_checker_.CalledOnValidThread());
356 // Account must have been seeded before attempting to add it.
357 DCHECK(!account_tracker_service_->GetAccountInfo(account_id).gaia.empty());
358 DCHECK(!account_tracker_service_->GetAccountInfo(account_id).email.empty());
360 bool account_present = accounts_.count(account_id) > 0;
361 if (account_present &&
362 accounts_[account_id]->GetAuthStatus().state() ==
363 GoogleServiceAuthError::NONE) {
364 // No need to update the account if it is already a known account and if
365 // there is no auth error.
369 if (!account_present) {
370 accounts_[account_id].reset(
371 new AccountStatus(signin_error_controller_, account_id));
374 UpdateAuthError(account_id, GoogleServiceAuthError::AuthErrorNone());
375 FireRefreshTokenAvailable(account_id);
378 void ProfileOAuth2TokenServiceIOSDelegate::RemoveAccount(
379 const std::string& account_id) {
380 DCHECK(thread_checker_.CalledOnValidThread());
381 DCHECK(!account_id.empty());
383 if (accounts_.count(account_id) > 0) {
384 accounts_.erase(account_id);
385 FireRefreshTokenRevoked(account_id);
389 std::set<std::string>
390 ProfileOAuth2TokenServiceIOSDelegate::GetExcludedSecondaryAccounts() {
391 const base::ListValue* excluded_secondary_accounts_pref =
392 client_->GetPrefs()->GetList(
393 prefs::kTokenServiceExcludedSecondaryAccounts);
394 std::set<std::string> excluded_secondary_accounts;
395 for (base::Value* pref_value : *excluded_secondary_accounts_pref) {
397 if (pref_value->GetAsString(&value))
398 excluded_secondary_accounts.insert(value);
400 return excluded_secondary_accounts;
403 void ProfileOAuth2TokenServiceIOSDelegate::ExcludeSecondaryAccounts(
404 const std::vector<std::string>& account_ids) {
405 for (const auto& account_id : account_ids)
406 ExcludeSecondaryAccount(account_id);
409 void ProfileOAuth2TokenServiceIOSDelegate::ExcludeSecondaryAccount(
410 const std::string& account_id) {
411 if (GetExcludeAllSecondaryAccounts()) {
412 // Avoid excluding individual secondary accounts when all secondary
413 // accounts are excluded.
417 DCHECK(!account_id.empty());
418 ListPrefUpdate update(client_->GetPrefs(),
419 prefs::kTokenServiceExcludedSecondaryAccounts);
420 base::ListValue* excluded_secondary_accounts = update.Get();
421 for (base::Value* pref_value : *excluded_secondary_accounts) {
422 std::string value_at_it;
423 if (pref_value->GetAsString(&value_at_it) && (value_at_it == account_id)) {
424 // |account_id| is already excluded.
428 excluded_secondary_accounts->AppendString(account_id);
431 void ProfileOAuth2TokenServiceIOSDelegate::IncludeSecondaryAccount(
432 const std::string& account_id) {
433 if (GetExcludeAllSecondaryAccounts()) {
434 // Avoid including individual secondary accounts when all secondary
435 // accounts are excluded.
439 DCHECK_NE(account_id, primary_account_id_);
440 DCHECK(!primary_account_id_.empty());
442 // Excluded secondary account ids is a logical set (not a list) of accounts.
443 // As the value stored in the excluded account ids preference is a list,
444 // the code below removes all occurences of |account_id| from this list. This
445 // ensures that |account_id| is actually included even in cases when the
446 // preference value was corrupted (see bug http://crbug.com/453470 as
448 ListPrefUpdate update(client_->GetPrefs(),
449 prefs::kTokenServiceExcludedSecondaryAccounts);
450 base::ListValue* excluded_secondary_accounts = update.Get();
451 base::ListValue::iterator it = excluded_secondary_accounts->begin();
452 while (it != excluded_secondary_accounts->end()) {
453 base::Value* pref_value = *it;
454 std::string value_at_it;
455 if (pref_value->GetAsString(&value_at_it) && (value_at_it == account_id)) {
456 it = excluded_secondary_accounts->Erase(it, nullptr);
463 bool ProfileOAuth2TokenServiceIOSDelegate::GetExcludeAllSecondaryAccounts() {
464 return client_->GetPrefs()->GetBoolean(
465 prefs::kTokenServiceExcludeAllSecondaryAccounts);
468 void ProfileOAuth2TokenServiceIOSDelegate::ExcludeAllSecondaryAccounts() {
469 client_->GetPrefs()->SetBoolean(
470 prefs::kTokenServiceExcludeAllSecondaryAccounts, true);
473 void ProfileOAuth2TokenServiceIOSDelegate::ClearExcludedSecondaryAccounts() {
474 client_->GetPrefs()->ClearPref(
475 prefs::kTokenServiceExcludeAllSecondaryAccounts);
476 client_->GetPrefs()->ClearPref(prefs::kTokenServiceExcludedSecondaryAccounts);
479 bool ProfileOAuth2TokenServiceIOSDelegate::IsAccountExcluded(
480 const std::string& gaia,
481 const std::string& email,
482 const std::set<std::string>& excluded_account_ids) {
483 std::string account_id =
484 account_tracker_service_->PickAccountIdForAccount(gaia, email);
485 if (account_id == primary_account_id_) {
486 // Only secondary account ids are excluded.
490 if (GetExcludeAllSecondaryAccounts())
492 return excluded_account_ids.count(account_id) > 0;
495 void ProfileOAuth2TokenServiceIOSDelegate::
496 MigrateExcludedSecondaryAccountIds() {
497 DCHECK_EQ(AccountTrackerService::MIGRATION_IN_PROGRESS,
498 account_tracker_service_->GetMigrationState());
500 // Before the account id migration, emails were used as account identifiers.
501 // Thus the pref |prefs::kTokenServiceExcludedSecondaryAccounts| holds the
502 // emails of the excluded secondary accounts.
503 std::set<std::string> excluded_emails = GetExcludedSecondaryAccounts();
504 if (excluded_emails.empty())
507 std::vector<std::string> excluded_account_ids;
508 for (const std::string& excluded_email : excluded_emails) {
509 ProfileOAuth2TokenServiceIOSProvider::AccountInfo account_info =
510 provider_->GetAccountInfoForEmail(excluded_email);
511 if (account_info.gaia.empty()) {
512 // The provider no longer has an account with email |excluded_email|.
513 // This can occur for 2 reasons:
514 // 1. The account with email |excluded_email| was removed before being
515 // migrated. It may simply be ignored in this case (no need to exclude
516 // an account that is no longer available).
517 // 2. The migration of the excluded account ids was already done before,
518 // but the entire migration of the accounts did not end for whatever
519 // reason (e.g. the app crashed during the previous attempt to migrate
520 // the accounts). The entire migration should be ignored in this case.
521 if (provider_->GetAccountInfoForGaia(excluded_email).gaia.empty()) {
522 // Case 1 above (account was removed).
523 DVLOG(1) << "Excluded secondary account with email " << excluded_email
524 << " was removed before migration.";
526 // Case 2 above (migration already done).
527 DVLOG(1) << "Excluded secondary account ids were already migrated.";
531 std::string excluded_account_id =
532 account_tracker_service_->PickAccountIdForAccount(account_info.gaia,
534 excluded_account_ids.push_back(excluded_account_id);
537 ClearExcludedSecondaryAccounts();
538 ExcludeSecondaryAccounts(excluded_account_ids);