Identity Service: Have GetPrimaryAccountWhenAvailable() check refresh token
https://chromium-review.googlesource.com/539637 introduced the
Identity Service GetPrimaryAccountWhenAvailable() method and changed the
identity extension API implementation to use it. However, that CL
inadvertently introduced a semantic change in the identity extension API
implementation in the case where the user was signed in with their refresh
token in an error state. Previously, the implementation would start a
signin flow and wait for a notification that a refresh token was available.
Crucially, this notification would come in only when a *new* refresh token
was available. GetPrimaryAccountWhenAvailable(), by contrast, returned
immediately if the user had a refresh token available (even if that
refresh token was in an error state). Due to a separate bug in the
identity API extension implementation itself, the net effect of this
semantic change is that the identity extension API implementation would loop
forever trying new requests and then going through the signin flow again
when those requests inevitably failed (because they were all being made
with the same bad refresh token).
This CL changes the semantics of the Identity Services'
GetPrimaryAccountWhenAvailable() method: it returns only when the user
has a primary account and that primary account has a refresh token *in a
non-error state.* This change, which better reflects the spirit of the
method, also restores the previous behavior of the identity extension API
implementation. Note that as the identity extension API implementation is
currently the only caller of this method in the codebase, this change
will have no other effects.
To test this change, do the following:
- Install an extension with the identity API in its manifest.
- Sign in to Chrome.
- Go to accounts.google.com and revoke your refresh token for Chrome there.
- Ensure that your account in Chrome goes into an error state (verify
that a red exclamation point appears in the top right by your account name).
- Go to chrome://extensions, enable developer mode, and inspect the background
page of the above app. At the JS console that that brings up, execute:
chrome.identity.getAuthToken({interactive: true}, (token) => {console.log(token);} )
- Verify that you can close the signin tab that then opens and continue
using the browser as normal.
- Sign in to Chrome.
- Verify that at the JS console referenced above a token gets printed.
This CL also adds a unittest that fails without the production change. As a
followup, I am going to investigate adding browsertests of the Chrome Identity
extension API that also would have failed before this change.
Bug: 772122
Change-Id: I544406684de1945b51807acac303bb667363615a
Reviewed-on: https://chromium-review.googlesource.com/704697
Reviewed-by: Mike West <[email protected]>
Reviewed-by: Mihai Sardarescu <[email protected]>
Commit-Queue: Colin Blundell (at a convergence, slow until Oct 16) <[email protected]>
Cr-Commit-Position: refs/heads/master@{#507394}
diff --git a/services/identity/DEPS b/services/identity/DEPS
index b4c64c1..f194cc9 100644
--- a/services/identity/DEPS
+++ b/services/identity/DEPS
@@ -8,5 +8,6 @@
"+components/signin/core/browser/test_signin_client.h",
"+components/signin/public",
"+components/sync_preferences/testing_pref_service_syncable.h",
+ "+google_apis/gaia/fake_oauth2_token_service_delegate.h",
"+google_apis/gaia/google_service_auth_error.h",
]
diff --git a/services/identity/README.md b/services/identity/README.md
index ad7f4c3..7a4feaf 100644
--- a/services/identity/README.md
+++ b/services/identity/README.md
@@ -61,9 +61,14 @@
or OAuth2TokenService::OnRefreshTokenIsAvailable() to determine when the primary
account is available, you should call
IdentityManager::GetPrimaryAccountWhenAvailable(). This method will fire when
-the authenticated account is signed in and has a refresh token available. Here
-is an [example CL](https://chromium-review.googlesource.com/c/539637/)
-illustrating this pattern.
+the authenticated account is signed in, has a refresh token available, and the
+refresh token is not in an error state. This method can be used in the context
+where the user is not yet signed in, as well as in the context where the user is
+signed in but in an auth error state, and the client wants to kick off a
+re-authentication flow and get notified when the re-authentication is complete
+and the user is no long in an auth error state. Here is an [example
+CL](https://chromium-review.googlesource.com/c/539637/) illustrating this
+pattern.
## Determining if an Account Has a Refresh Token Available
diff --git a/services/identity/identity_manager.cc b/services/identity/identity_manager.cc
index 4c71982..0eff50b 100644
--- a/services/identity/identity_manager.cc
+++ b/services/identity/identity_manager.cc
@@ -105,7 +105,8 @@
AccountInfo account_info = signin_manager_->GetAuthenticatedAccountInfo();
AccountState account_state = GetStateOfAccount(account_info);
- if (!account_state.has_refresh_token) {
+ if (!account_state.has_refresh_token ||
+ token_service_->RefreshTokenHasError(account_info.account_id)) {
primary_account_available_callbacks_.push_back(std::move(callback));
return;
}
@@ -172,7 +173,8 @@
// Check whether the primary account is available and notify any waiting
// consumers if so.
- if (account_state.is_primary_account && account_state.has_refresh_token) {
+ if (account_state.is_primary_account && account_state.has_refresh_token &&
+ !token_service_->RefreshTokenHasError(account_info.account_id)) {
DCHECK(!account_info.account_id.empty());
DCHECK(!account_info.email.empty());
DCHECK(!account_info.gaia.empty());
diff --git a/services/identity/identity_manager_unittest.cc b/services/identity/identity_manager_unittest.cc
index 1cbc78f..c6f7b43e 100644
--- a/services/identity/identity_manager_unittest.cc
+++ b/services/identity/identity_manager_unittest.cc
@@ -10,6 +10,7 @@
#include "components/signin/core/browser/fake_signin_manager.h"
#include "components/signin/core/browser/test_signin_client.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
+#include "google_apis/gaia/fake_oauth2_token_service_delegate.h"
#include "mojo/public/cpp/bindings/binding_set.h"
#include "services/identity/identity_service.h"
#include "services/identity/public/cpp/account_state.h"
@@ -539,6 +540,49 @@
EXPECT_TRUE(account_state2.is_primary_account);
}
+// Check that GetPrimaryAccountWhenAvailable() doesn't return the account as
+// available if the refresh token has an auth error.
+TEST_F(IdentityManagerTest,
+ GetPrimaryAccountWhenAvailableRefreshTokenHasAuthError) {
+ signin_manager()->SetAuthenticatedAccountInfo(kTestGaiaId, kTestEmail);
+ token_service()->UpdateCredentials(
+ signin_manager()->GetAuthenticatedAccountId(), kTestRefreshToken);
+ FakeOAuth2TokenServiceDelegate* delegate =
+ static_cast<FakeOAuth2TokenServiceDelegate*>(
+ token_service()->GetDelegate());
+ delegate->SetLastErrorForAccount(
+ signin_manager()->GetAuthenticatedAccountId(),
+ GoogleServiceAuthError(
+ GoogleServiceAuthError::State::INVALID_GAIA_CREDENTIALS));
+
+ AccountInfo account_info;
+ AccountState account_state;
+ base::RunLoop run_loop;
+ GetIdentityManager()->GetPrimaryAccountWhenAvailable(base::Bind(
+ &IdentityManagerTest::OnPrimaryAccountAvailable, base::Unretained(this),
+ run_loop.QuitClosure(), base::Unretained(&account_info),
+ base::Unretained(&account_state)));
+
+ // Flush the Identity Manager and check that the callback didn't fire.
+ FlushIdentityManagerForTesting();
+ EXPECT_TRUE(account_info.account_id.empty());
+
+ // Clear the auth error, update credentials, and check that the callback
+ // fires.
+ delegate->SetLastErrorForAccount(
+ signin_manager()->GetAuthenticatedAccountId(), GoogleServiceAuthError());
+ token_service()->UpdateCredentials(
+ signin_manager()->GetAuthenticatedAccountId(), kTestRefreshToken);
+ run_loop.Run();
+
+ EXPECT_EQ(signin_manager()->GetAuthenticatedAccountId(),
+ account_info.account_id);
+ EXPECT_EQ(kTestGaiaId, account_info.gaia);
+ EXPECT_EQ(kTestEmail, account_info.email);
+ EXPECT_TRUE(account_state.has_refresh_token);
+ EXPECT_TRUE(account_state.is_primary_account);
+}
+
// Check that the account info for a given GAIA ID is null if that GAIA ID is
// unknown.
TEST_F(IdentityManagerTest, GetAccountInfoForUnknownGaiaID) {
diff --git a/services/identity/public/interfaces/identity_manager.mojom b/services/identity/public/interfaces/identity_manager.mojom
index 20ecde0..1f31f2ac 100644
--- a/services/identity/public/interfaces/identity_manager.mojom
+++ b/services/identity/public/interfaces/identity_manager.mojom
@@ -22,9 +22,10 @@
// Returns the AccountInfo for the Google account that serves as the user's
// primary account once this account is available (i.e., the user is signed
- // in and a refresh token is available). |account_state| gives the current
- // state of the account. Overlapping requests are permitted; all pending
- // requests will be called back when the primary account is available.
+ // in, a refresh token is available, and the refresh token is in a non-error
+ // state). |account_state| gives the current state of the account.
+ // Overlapping requests are permitted; all pending requests will be called
+ // back when the primary account is available.
GetPrimaryAccountWhenAvailable() => (AccountInfo account_info,
AccountState account_state);