[go: nahoru, domu]

blob: 075062200488e4f3e45cd8f63577d38b143267f1 [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/signin/internal/identity_manager/oauth_multilogin_helper.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/test/task_environment.h"
#include "components/prefs/testing_pref_service.h"
#include "components/signin/internal/identity_manager/fake_profile_oauth2_token_service.h"
#include "components/signin/public/base/test_signin_client.h"
#include "components/signin/public/identity_manager/accounts_cookie_mutator.h"
#include "components/signin/public/identity_manager/set_accounts_in_cookie_result.h"
#include "google_apis/gaia/core_account_id.h"
#include "google_apis/gaia/gaia_urls.h"
#include "services/network/test/test_cookie_manager.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace signin {
namespace {
constexpr char kGaiaId[] = "gaia_id_1";
constexpr char kGaiaId2[] = "gaia_id_2";
constexpr char kAccessToken[] = "access_token_1";
constexpr char kAccessToken2[] = "access_token_2";
const char kExternalCcResult[] = "youtube:OK";
constexpr int kMaxFetcherRetries = 3;
const char kMultiloginSuccessResponse[] =
R"()]}'
{
"status": "OK",
"cookies":[
{
"name":"SID",
"value":"SID_value",
"domain":".google.fr",
"path":"/",
"isSecure":true,
"isHttpOnly":false,
"priority":"HIGH",
"maxAge":63070000
}
]
}
)";
const char kMultiloginSuccessResponseTwoCookies[] =
R"()]}'
{
"status": "OK",
"cookies":[
{
"name":"SID",
"value":"SID_value",
"domain":".google.fr",
"path":"/",
"isSecure":true,
"isHttpOnly":false,
"priority":"HIGH",
"maxAge":63070000
},
{
"name":"FOO",
"value":"FOO_value",
"domain":".google.com",
"path":"/",
"isSecure":true,
"isHttpOnly":false,
"priority":"HIGH",
"maxAge":63070000
}
]
}
)";
const char kMultiloginSuccessResponseWithSecondaryDomain[] =
R"()]}'
{
"status": "OK",
"cookies":[
{
"name":"SID",
"value":"SID_value",
"domain":".youtube.com",
"path":"/",
"isSecure":true,
"isHttpOnly":false,
"priority":"HIGH",
"maxAge":63070000
},
{
"name":"FOO",
"value":"FOO_value",
"domain":".google.com",
"path":"/",
"isSecure":true,
"isHttpOnly":false,
"priority":"HIGH",
"maxAge":63070000
}
]
}
)";
const char kMultiloginRetryResponse[] =
R"()]}'
{
"status": "RETRY"
}
)";
const char kMultiloginInvalidTokenResponse[] =
R"()]}'
{
"status": "INVALID_TOKENS",
"failed_accounts": [
{ "obfuscated_id": "gaia_id_1", "status": "RECOVERABLE" },
{ "obfuscated_id": "gaia_id_2", "status": "OK" }
]
}
)";
// GMock matcher that checks that the cookie has the expected parameters.
MATCHER_P3(CookieMatcher, name, value, domain, "") {
return arg.Name() == name && arg.Value() == value && arg.Domain() == domain &&
arg.Path() == "/" && arg.SecureAttribute() && !arg.IsHttpOnly();
}
// Checks that the argument (a GURL) is secure and has the given hostname.
MATCHER_P(CookieSourceMatcher, cookie_host, "") {
return arg.is_valid() && arg.scheme() == "https" && arg.host() == cookie_host;
}
void RunSetCookieCallbackWithSuccess(
const net::CanonicalCookie&,
const GURL&,
const net::CookieOptions&,
network::mojom::CookieManager::SetCanonicalCookieCallback callback) {
std::move(callback).Run(net::CookieAccessResult());
}
class MockCookieManager
: public ::testing::StrictMock<network::TestCookieManager> {
public:
MOCK_METHOD4(SetCanonicalCookie,
void(const net::CanonicalCookie& cookie,
const GURL& source_url,
const net::CookieOptions& cookie_options,
SetCanonicalCookieCallback callback));
};
class MockTokenService : public FakeProfileOAuth2TokenService {
public:
explicit MockTokenService(PrefService* prefs)
: FakeProfileOAuth2TokenService(prefs) {}
MOCK_METHOD2(InvalidateTokenForMultilogin,
void(const CoreAccountId& account_id, const std::string& token));
};
} // namespace
class OAuthMultiloginHelperTest
: public testing::Test,
public AccountsCookieMutator::PartitionDelegate {
public:
OAuthMultiloginHelperTest()
: kAccountId(CoreAccountId::FromGaiaId(kGaiaId)),
kAccountId2(CoreAccountId::FromGaiaId(kGaiaId2)),
test_signin_client_(&pref_service_),
mock_token_service_(&pref_service_) {}
~OAuthMultiloginHelperTest() override = default;
std::unique_ptr<OAuthMultiloginHelper> CreateHelper(
const std::vector<OAuthMultiloginHelper::AccountIdGaiaIdPair> accounts) {
return std::make_unique<OAuthMultiloginHelper>(
&test_signin_client_, this, token_service(),
gaia::MultiloginMode::MULTILOGIN_UPDATE_COOKIE_ACCOUNTS_ORDER, accounts,
std::string(), gaia::GaiaSource::kChrome,
base::BindOnce(&OAuthMultiloginHelperTest::OnOAuthMultiloginFinished,
base::Unretained(this)));
}
std::unique_ptr<OAuthMultiloginHelper> CreateHelperWithExternalCcResult(
const std::vector<OAuthMultiloginHelper::AccountIdGaiaIdPair> accounts) {
return std::make_unique<OAuthMultiloginHelper>(
&test_signin_client_, this, token_service(),
gaia::MultiloginMode::MULTILOGIN_UPDATE_COOKIE_ACCOUNTS_ORDER, accounts,
kExternalCcResult, gaia::GaiaSource::kChrome,
base::BindOnce(&OAuthMultiloginHelperTest::OnOAuthMultiloginFinished,
base::Unretained(this)));
}
network::TestURLLoaderFactory* url_loader() {
return test_signin_client_.GetTestURLLoaderFactory();
}
std::string multilogin_url() const {
return GaiaUrls::GetInstance()->oauth_multilogin_url().spec() +
"?source=ChromiumBrowser&reuseCookies=0";
}
std::string multilogin_url_with_external_cc_result() const {
return GaiaUrls::GetInstance()->oauth_multilogin_url().spec() +
"?source=ChromiumBrowser&reuseCookies=0&externalCcResult=" +
kExternalCcResult;
}
MockCookieManager* cookie_manager() { return &mock_cookie_manager_; }
MockTokenService* token_service() { return &mock_token_service_; }
protected:
void OnOAuthMultiloginFinished(SetAccountsInCookieResult result) {
DCHECK(!callback_called_);
callback_called_ = true;
result_ = result;
}
// AccountsCookieMuator::PartitionDelegate:
std::unique_ptr<GaiaAuthFetcher> CreateGaiaAuthFetcherForPartition(
GaiaAuthConsumer* consumer,
const gaia::GaiaSource& source) override {
return test_signin_client_.CreateGaiaAuthFetcher(consumer, source);
}
network::mojom::CookieManager* GetCookieManagerForPartition() override {
return &mock_cookie_manager_;
}
const CoreAccountId kAccountId;
const CoreAccountId kAccountId2;
base::test::TaskEnvironment task_environment_;
bool callback_called_ = false;
SetAccountsInCookieResult result_;
TestingPrefServiceSimple pref_service_;
MockCookieManager mock_cookie_manager_;
TestSigninClient test_signin_client_;
MockTokenService mock_token_service_;
};
// Everything succeeds.
TEST_F(OAuthMultiloginHelperTest, Success) {
token_service()->UpdateCredentials(kAccountId, "refresh_token");
std::unique_ptr<OAuthMultiloginHelper> helper =
CreateHelper({{kAccountId, kGaiaId}});
// Configure mock cookie manager:
// - check that the cookie is the expected one
// - immediately invoke the callback
EXPECT_CALL(*cookie_manager(),
SetCanonicalCookie(
CookieMatcher("SID", "SID_value", ".google.fr"),
CookieSourceMatcher("google.fr"), testing::_, testing::_))
.WillOnce(::testing::Invoke(RunSetCookieCallbackWithSuccess));
// Issue access token.
OAuth2AccessTokenConsumer::TokenResponse success_response;
success_response.access_token = kAccessToken;
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
// Multilogin call.
EXPECT_FALSE(callback_called_);
EXPECT_TRUE(url_loader()->IsPending(multilogin_url()));
url_loader()->AddResponse(multilogin_url(), kMultiloginSuccessResponse);
EXPECT_FALSE(url_loader()->IsPending(multilogin_url()));
EXPECT_TRUE(callback_called_);
EXPECT_EQ(SetAccountsInCookieResult::kSuccess, result_);
}
// Multiple cookies in the multilogin response.
TEST_F(OAuthMultiloginHelperTest, MultipleCookies) {
token_service()->UpdateCredentials(kAccountId, "refresh_token");
std::unique_ptr<OAuthMultiloginHelper> helper =
CreateHelper({{kAccountId, kGaiaId}});
// Configure mock cookie manager:
// - check that the cookie is the expected one
// - immediately invoke the callback
EXPECT_CALL(*cookie_manager(),
SetCanonicalCookie(
CookieMatcher("SID", "SID_value", ".google.fr"),
CookieSourceMatcher("google.fr"), testing::_, testing::_))
.WillOnce(::testing::Invoke(RunSetCookieCallbackWithSuccess));
EXPECT_CALL(*cookie_manager(),
SetCanonicalCookie(
CookieMatcher("FOO", "FOO_value", ".google.com"),
CookieSourceMatcher("google.com"), testing::_, testing::_))
.WillOnce(::testing::Invoke(RunSetCookieCallbackWithSuccess));
// Issue access token.
OAuth2AccessTokenConsumer::TokenResponse success_response;
success_response.access_token = kAccessToken;
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
// Multilogin call.
EXPECT_FALSE(callback_called_);
EXPECT_TRUE(url_loader()->IsPending(multilogin_url()));
url_loader()->AddResponse(multilogin_url(),
kMultiloginSuccessResponseTwoCookies);
EXPECT_FALSE(url_loader()->IsPending(multilogin_url()));
EXPECT_TRUE(callback_called_);
EXPECT_EQ(SetAccountsInCookieResult::kSuccess, result_);
}
// Multiple cookies in the multilogin response.
TEST_F(OAuthMultiloginHelperTest, SuccessWithExternalCcResult) {
token_service()->UpdateCredentials(kAccountId, "refresh_token");
std::unique_ptr<OAuthMultiloginHelper> helper =
CreateHelperWithExternalCcResult({{kAccountId, kGaiaId}});
// Configure mock cookie manager:
// - check that the cookie is the expected one
// - immediately invoke the callback
EXPECT_CALL(*cookie_manager(),
SetCanonicalCookie(
CookieMatcher("SID", "SID_value", ".youtube.com"),
CookieSourceMatcher("youtube.com"), testing::_, testing::_))
.WillOnce(::testing::Invoke(RunSetCookieCallbackWithSuccess));
EXPECT_CALL(*cookie_manager(),
SetCanonicalCookie(
CookieMatcher("FOO", "FOO_value", ".google.com"),
CookieSourceMatcher("google.com"), testing::_, testing::_))
.WillOnce(::testing::Invoke(RunSetCookieCallbackWithSuccess));
// Issue access token.
OAuth2AccessTokenConsumer::TokenResponse success_response;
success_response.access_token = kAccessToken;
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
// Multilogin call.
EXPECT_FALSE(callback_called_);
EXPECT_TRUE(
url_loader()->IsPending(multilogin_url_with_external_cc_result()));
url_loader()->AddResponse(multilogin_url_with_external_cc_result(),
kMultiloginSuccessResponseWithSecondaryDomain);
EXPECT_FALSE(
url_loader()->IsPending(multilogin_url_with_external_cc_result()));
EXPECT_TRUE(callback_called_);
EXPECT_EQ(SetAccountsInCookieResult::kSuccess, result_);
}
// Failure to get the access token.
TEST_F(OAuthMultiloginHelperTest, OneAccountAccessTokenFailure) {
token_service()->UpdateCredentials(kAccountId, "refresh_token");
std::unique_ptr<OAuthMultiloginHelper> helper =
CreateHelper({{kAccountId, kGaiaId}});
token_service()->IssueErrorForAllPendingRequestsForAccount(
kAccountId,
GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
EXPECT_TRUE(callback_called_);
EXPECT_EQ(SetAccountsInCookieResult::kPersistentError, result_);
}
// Retry on transient errors in the multilogin call.
TEST_F(OAuthMultiloginHelperTest, OneAccountTransientMultiloginError) {
token_service()->UpdateCredentials(kAccountId, "refresh_token");
std::unique_ptr<OAuthMultiloginHelper> helper =
CreateHelper({{kAccountId, kGaiaId}});
// Configure mock cookie manager:
// - check that the cookie is the expected one
// - immediately invoke the callback
EXPECT_CALL(*cookie_manager(),
SetCanonicalCookie(
CookieMatcher("SID", "SID_value", ".google.fr"),
CookieSourceMatcher("google.fr"), testing::_, testing::_))
.WillOnce(::testing::Invoke(RunSetCookieCallbackWithSuccess));
// Issue access token.
OAuth2AccessTokenConsumer::TokenResponse success_response;
success_response.access_token = kAccessToken;
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
// Multilogin call fails with transient error.
EXPECT_TRUE(url_loader()->IsPending(multilogin_url()));
url_loader()->SimulateResponseForPendingRequest(multilogin_url(),
kMultiloginRetryResponse);
// Call is retried and succeeds.
EXPECT_FALSE(url_loader()->IsPending(multilogin_url()));
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
EXPECT_FALSE(callback_called_);
EXPECT_TRUE(url_loader()->IsPending(multilogin_url()));
url_loader()->AddResponse(multilogin_url(), kMultiloginSuccessResponse);
EXPECT_FALSE(url_loader()->IsPending(multilogin_url()));
EXPECT_TRUE(callback_called_);
EXPECT_EQ(SetAccountsInCookieResult::kSuccess, result_);
}
// Stop retrying after too many transient errors in the multilogin call.
TEST_F(OAuthMultiloginHelperTest,
OneAccountTransientMultiloginErrorMaxRetries) {
token_service()->UpdateCredentials(kAccountId, "refresh_token");
std::unique_ptr<OAuthMultiloginHelper> helper =
CreateHelper({{kAccountId, kGaiaId}});
// Issue access token.
OAuth2AccessTokenConsumer::TokenResponse success_response;
success_response.access_token = kAccessToken;
// Multilogin call fails with transient error, retry many times.
for (int i = 0; i < kMaxFetcherRetries; ++i) {
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
EXPECT_TRUE(url_loader()->IsPending(multilogin_url()));
EXPECT_FALSE(callback_called_);
url_loader()->SimulateResponseForPendingRequest(multilogin_url(),
kMultiloginRetryResponse);
}
// Failure after exceeding the maximum number of retries.
EXPECT_TRUE(callback_called_);
EXPECT_EQ(SetAccountsInCookieResult::kTransientError, result_);
}
// Persistent error in the multilogin call.
TEST_F(OAuthMultiloginHelperTest, OneAccountPersistentMultiloginError) {
token_service()->UpdateCredentials(kAccountId, "refresh_token");
std::unique_ptr<OAuthMultiloginHelper> helper =
CreateHelper({{kAccountId, kGaiaId}});
// Issue access token.
OAuth2AccessTokenConsumer::TokenResponse success_response;
success_response.access_token = kAccessToken;
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
// Multilogin call fails with persistent error.
EXPECT_FALSE(callback_called_);
EXPECT_TRUE(url_loader()->IsPending(multilogin_url()));
url_loader()->AddResponse(multilogin_url(), "blah"); // Unexpected response.
EXPECT_FALSE(url_loader()->IsPending(multilogin_url()));
EXPECT_TRUE(callback_called_);
EXPECT_EQ(SetAccountsInCookieResult::kPersistentError, result_);
}
// Retry on "invalid token" in the multilogin response.
TEST_F(OAuthMultiloginHelperTest, InvalidTokenError) {
token_service()->UpdateCredentials(kAccountId, "refresh_token");
token_service()->UpdateCredentials(kAccountId2, "refresh_token");
std::unique_ptr<OAuthMultiloginHelper> helper =
CreateHelper({{kAccountId, kGaiaId}, {kAccountId2, kGaiaId2}});
// Configure mock cookie manager:
// - check that the cookie is the expected one
// - immediately invoke the callback
EXPECT_CALL(*cookie_manager(),
SetCanonicalCookie(
CookieMatcher("SID", "SID_value", ".google.fr"),
CookieSourceMatcher("google.fr"), testing::_, testing::_))
.WillOnce(::testing::Invoke(RunSetCookieCallbackWithSuccess));
// The failed access token should be invalidated.
EXPECT_CALL(*token_service(),
InvalidateTokenForMultilogin(kAccountId, kAccessToken));
// Issue access tokens.
OAuth2AccessTokenConsumer::TokenResponse success_response;
success_response.access_token = kAccessToken;
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
OAuth2AccessTokenConsumer::TokenResponse success_response_2;
success_response_2.access_token = kAccessToken2;
token_service()->IssueAllTokensForAccount(kAccountId2, success_response_2);
// Multilogin call fails with invalid token for kAccountId.
EXPECT_TRUE(url_loader()->IsPending(multilogin_url()));
url_loader()->SimulateResponseForPendingRequest(
multilogin_url(), kMultiloginInvalidTokenResponse);
// Both tokens are retried.
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
EXPECT_FALSE(callback_called_);
EXPECT_FALSE(url_loader()->IsPending(multilogin_url()));
token_service()->IssueAllTokensForAccount(kAccountId2, success_response);
// Multilogin succeeds the second time.
EXPECT_FALSE(callback_called_);
EXPECT_TRUE(url_loader()->IsPending(multilogin_url()));
url_loader()->AddResponse(multilogin_url(), kMultiloginSuccessResponse);
EXPECT_FALSE(url_loader()->IsPending(multilogin_url()));
EXPECT_TRUE(callback_called_);
EXPECT_EQ(SetAccountsInCookieResult::kSuccess, result_);
}
// Retry on "invalid token" in the multilogin response.
TEST_F(OAuthMultiloginHelperTest, InvalidTokenErrorMaxRetries) {
token_service()->UpdateCredentials(kAccountId, "refresh_token");
token_service()->UpdateCredentials(kAccountId2, "refresh_token");
std::unique_ptr<OAuthMultiloginHelper> helper =
CreateHelper({{kAccountId, kGaiaId}, {kAccountId2, kGaiaId2}});
// The failed access token should be invalidated.
EXPECT_CALL(*token_service(),
InvalidateTokenForMultilogin(kAccountId, kAccessToken))
.Times(kMaxFetcherRetries);
// Issue access tokens.
OAuth2AccessTokenConsumer::TokenResponse success_response;
success_response.access_token = kAccessToken;
OAuth2AccessTokenConsumer::TokenResponse success_response_2;
success_response_2.access_token = kAccessToken2;
// Multilogin call fails with invalid token for kAccountId. Retry many times.
for (int i = 0; i < kMaxFetcherRetries; ++i) {
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
EXPECT_FALSE(url_loader()->IsPending(multilogin_url()));
token_service()->IssueAllTokensForAccount(kAccountId2, success_response_2);
EXPECT_FALSE(callback_called_);
EXPECT_TRUE(url_loader()->IsPending(multilogin_url()));
url_loader()->SimulateResponseForPendingRequest(
multilogin_url(), kMultiloginInvalidTokenResponse);
}
// The maximum number of retries is reached, fail.
EXPECT_FALSE(url_loader()->IsPending(multilogin_url()));
EXPECT_TRUE(callback_called_);
EXPECT_EQ(SetAccountsInCookieResult::kTransientError, result_);
}
} // namespace signin