blob: ac51c70115e4236d21dd7784606c644ab464f258 [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/device_identity/device_oauth2_token_service.h"
#include <stdint.h>
#include <memory>
#include <set>
#include <utility>
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "base/threading/thread_task_runner_handle.h"
#include "build/chromeos_buildflags.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/scoped_testing_local_state.h"
#include "chrome/test/base/testing_browser_process.h"
#include "components/prefs/testing_pref_service.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_utils.h"
#include "google_apis/gaia/gaia_oauth_client.h"
#include "google_apis/gaia/gaia_urls.h"
#include "google_apis/gaia/oauth2_access_token_manager_test_util.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "services/network/test/test_url_loader_factory.h"
#include "services/network/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
const char kRobotEmail[] = "service_acct@system.gserviceaccount.com";
const char kWrongRobotEmail[] = "WRONG_service_acct@system.gserviceaccount.com";
} // namespace
class MockDeviceOAuth2TokenStore : public DeviceOAuth2TokenStore {
public:
MockDeviceOAuth2TokenStore() = default;
~MockDeviceOAuth2TokenStore() override = default;
// DeviceOAuth2TokenStore:
void Init(InitCallback callback) override {
pending_init_callback_ = std::move(callback);
}
CoreAccountId GetAccountId() const override { return account_id_; }
std::string GetRefreshToken() const override { return refresh_token_; }
void SetAndSaveRefreshToken(const std::string& refresh_token,
StatusCallback result_callback) override {
refresh_token_ = refresh_token;
pending_status_callback_ = std::move(result_callback);
}
void PrepareTrustedAccountId(TrustedAccountIdCallback callback) override {
pending_trusted_account_id_callback_ = std::move(callback);
TriggerTrustedAccountIdCallback(true);
}
#if !BUILDFLAG(IS_CHROMEOS_ASH)
void SetAccountEmail(const std::string& account_email) override {
account_id_ = CoreAccountId::FromRobotEmail(account_email);
}
#endif
// Mock-specific functions:
void SetRefreshTokenForTesting(const std::string& token) {
refresh_token_ = token;
}
void SetAccountIdForTesting(CoreAccountId account_id) {
account_id_ = account_id;
}
void TriggerInitCallback(bool success, bool validation_required) {
std::move(pending_init_callback_).Run(success, validation_required);
}
void TriggerStatusCallback(bool success) {
std::move(pending_status_callback_).Run(success);
}
void TriggerTrustedAccountIdCallback(bool account_present) {
std::move(pending_trusted_account_id_callback_).Run(account_present);
}
private:
CoreAccountId account_id_;
std::string refresh_token_;
InitCallback pending_init_callback_;
StatusCallback pending_status_callback_;
TrustedAccountIdCallback pending_trusted_account_id_callback_;
};
class DeviceOAuth2TokenServiceTest : public testing::Test {
public:
DeviceOAuth2TokenServiceTest()
: scoped_testing_local_state_(TestingBrowserProcess::GetGlobal()) {}
// Most tests just want a noop crypto impl with a dummy refresh token value in
// Local State (if the value is an empty string, it will be ignored).
void SetUpDefaultValues() {
CreateService();
token_store_->SetRefreshTokenForTesting("device_refresh_token_4_test");
SetRobotAccountId(kRobotEmail);
AssertConsumerTokensAndErrors(0, 0);
token_store_->TriggerInitCallback(true, true);
}
void SetRobotAccountId(const std::string& robot_email) {
token_store_->SetAccountIdForTesting(
CoreAccountId::FromRobotEmail(robot_email));
}
std::unique_ptr<OAuth2AccessTokenManager::Request> StartTokenRequest() {
return oauth2_service_->StartAccessTokenRequest(std::set<std::string>(),
&consumer_);
}
void SetUp() override {}
void TearDown() override {
oauth2_service_.reset();
base::ThreadPoolInstance::Get()->FlushForTesting();
base::RunLoop().RunUntilIdle();
}
MockDeviceOAuth2TokenStore* token_store() { return token_store_; }
void CreateService() {
auto store = std::make_unique<MockDeviceOAuth2TokenStore>();
token_store_ = store.get();
oauth2_service_.reset(new DeviceOAuth2TokenService(
test_url_loader_factory_.GetSafeWeakWrapper(), std::move(store)));
oauth2_service_->max_refresh_token_validation_retries_ = 0;
oauth2_service_->GetAccessTokenManager()
->set_max_authorization_token_fetch_retries_for_testing(0);
}
std::string GetValidTokenInfoResponse(const std::string& email) {
return "{ \"email\": \"" + email +
"\","
" \"user_id\": \"1234567890\" }";
}
std::string GetInvalidScopeResponse(const std::string& scope) {
return "{ \"error\": \"invalid_scope\", "
"\"error_description\": \"Some requested scopes were invalid. "
"{invalid\\u003d[" +
scope +
"}\", "
"\"error_uri\": "
"\"https://developers.google.com/identity/protocols/oauth2\""
"}";
}
bool RefreshTokenIsAvailable() {
return oauth2_service_->RefreshTokenIsAvailable();
}
std::string GetRefreshToken() {
if (!RefreshTokenIsAvailable())
return std::string();
return oauth2_service_->GetRefreshToken();
}
// A utility method to return fake URL results, for testing the refresh token
// validation logic. For a successful validation attempt, this method will be
// called three times for the steps listed below.
//
// Step 1a: fetch the access token for the tokeninfo API.
// Step 1b: call the tokeninfo API.
// Step 2: Fetch the access token for the requested scope
// (in this case, cloudprint).
void ReturnOAuthUrlFetchResults(const std::string& url,
net::HttpStatusCode response_code,
const std::string& response_string);
// Generates URL fetch replies with the specified results for requests
// generated by the token service.
void PerformURLFetchesWithResults(
net::HttpStatusCode tokeninfo_access_token_status,
const std::string& tokeninfo_access_token_response,
net::HttpStatusCode tokeninfo_fetch_status,
const std::string& tokeninfo_fetch_response,
net::HttpStatusCode service_access_token_status,
const std::string& service_access_token_response);
// Generates URL fetch replies for the success path.
void PerformURLFetches();
void AssertConsumerTokensAndErrors(int num_tokens, int num_errors);
protected:
// This is here because DeviceOAuth2TokenService's destructor is private;
// base::DefaultDeleter therefore doesn't work. However, the test class is
// declared friend in DeviceOAuth2TokenService, so this deleter works.
struct TokenServiceDeleter {
inline void operator()(DeviceOAuth2TokenService* ptr) const { delete ptr; }
};
content::BrowserTaskEnvironment task_environment_;
ScopedTestingLocalState scoped_testing_local_state_;
network::TestURLLoaderFactory test_url_loader_factory_;
std::unique_ptr<DeviceOAuth2TokenService, TokenServiceDeleter>
oauth2_service_;
TestingOAuth2AccessTokenManagerConsumer consumer_;
raw_ptr<MockDeviceOAuth2TokenStore> token_store_;
};
void DeviceOAuth2TokenServiceTest::ReturnOAuthUrlFetchResults(
const std::string& url,
net::HttpStatusCode response_code,
const std::string& response_string) {
if (test_url_loader_factory_.IsPending(url)) {
test_url_loader_factory_.SimulateResponseForPendingRequest(
GURL(url), network::URLLoaderCompletionStatus(net::OK),
network::CreateURLResponseHead(response_code), response_string);
}
}
void DeviceOAuth2TokenServiceTest::PerformURLFetchesWithResults(
net::HttpStatusCode tokeninfo_access_token_status,
const std::string& tokeninfo_access_token_response,
net::HttpStatusCode tokeninfo_fetch_status,
const std::string& tokeninfo_fetch_response,
net::HttpStatusCode service_access_token_status,
const std::string& service_access_token_response) {
ReturnOAuthUrlFetchResults(GaiaUrls::GetInstance()->oauth2_token_url().spec(),
tokeninfo_access_token_status,
tokeninfo_access_token_response);
ReturnOAuthUrlFetchResults(
GaiaUrls::GetInstance()->oauth2_token_info_url().spec(),
tokeninfo_fetch_status, tokeninfo_fetch_response);
ReturnOAuthUrlFetchResults(GaiaUrls::GetInstance()->oauth2_token_url().spec(),
service_access_token_status,
service_access_token_response);
}
void DeviceOAuth2TokenServiceTest::PerformURLFetches() {
PerformURLFetchesWithResults(
net::HTTP_OK, GetValidTokenResponse("tokeninfo_access_token", 3600),
net::HTTP_OK, GetValidTokenInfoResponse(kRobotEmail), net::HTTP_OK,
GetValidTokenResponse("scoped_access_token", 3600));
}
void DeviceOAuth2TokenServiceTest::AssertConsumerTokensAndErrors(
int num_tokens,
int num_errors) {
EXPECT_EQ(num_tokens, consumer_.number_of_successful_tokens_);
EXPECT_EQ(num_errors, consumer_.number_of_errors_);
}
TEST_F(DeviceOAuth2TokenServiceTest, RefreshTokenValidation_Success) {
SetUpDefaultValues();
std::unique_ptr<OAuth2AccessTokenManager::Request> request =
StartTokenRequest();
PerformURLFetches();
AssertConsumerTokensAndErrors(1, 0);
EXPECT_EQ("scoped_access_token", consumer_.last_token_);
}
TEST_F(DeviceOAuth2TokenServiceTest, RefreshTokenValidation_SuccessAsyncLoad) {
CreateService();
token_store()->SetRefreshTokenForTesting("device_refresh_token_4_test");
SetRobotAccountId(kRobotEmail);
std::unique_ptr<OAuth2AccessTokenManager::Request> request =
StartTokenRequest();
PerformURLFetches();
AssertConsumerTokensAndErrors(0, 0);
token_store()->TriggerInitCallback(true, true);
base::RunLoop().RunUntilIdle();
PerformURLFetches();
AssertConsumerTokensAndErrors(1, 0);
EXPECT_EQ("scoped_access_token", consumer_.last_token_);
}
TEST_F(DeviceOAuth2TokenServiceTest, RefreshTokenValidation_Cancel) {
SetUpDefaultValues();
std::unique_ptr<OAuth2AccessTokenManager::Request> request =
StartTokenRequest();
request.reset();
PerformURLFetches();
// Test succeeds if this line is reached without a crash.
}
TEST_F(DeviceOAuth2TokenServiceTest, RefreshTokenValidation_InitFailure) {
CreateService();
token_store()->SetRefreshTokenForTesting("device_refresh_token_4_test");
SetRobotAccountId(kRobotEmail);
token_store()->TriggerInitCallback(false, true);
EXPECT_FALSE(RefreshTokenIsAvailable());
std::unique_ptr<OAuth2AccessTokenManager::Request> request =
StartTokenRequest();
base::RunLoop().RunUntilIdle();
AssertConsumerTokensAndErrors(0, 1);
}
TEST_F(DeviceOAuth2TokenServiceTest,
RefreshTokenValidation_Failure_TokenInfoAccessTokenHttpError) {
SetUpDefaultValues();
std::unique_ptr<OAuth2AccessTokenManager::Request> request =
StartTokenRequest();
PerformURLFetchesWithResults(net::HTTP_UNAUTHORIZED, "", net::HTTP_OK,
GetValidTokenInfoResponse(kRobotEmail),
net::HTTP_OK,
GetValidTokenResponse("ignored", 3600));
AssertConsumerTokensAndErrors(0, 1);
}
TEST_F(DeviceOAuth2TokenServiceTest,
RefreshTokenValidation_Failure_TokenInfoAccessTokenInvalidResponse) {
SetUpDefaultValues();
std::unique_ptr<OAuth2AccessTokenManager::Request> request =
StartTokenRequest();
PerformURLFetchesWithResults(net::HTTP_OK, "invalid response", net::HTTP_OK,
GetValidTokenInfoResponse(kRobotEmail),
net::HTTP_OK,
GetValidTokenResponse("ignored", 3600));
AssertConsumerTokensAndErrors(0, 1);
}
TEST_F(DeviceOAuth2TokenServiceTest,
RefreshTokenValidation_Failure_InvalidScope) {
SetUpDefaultValues();
std::unique_ptr<OAuth2AccessTokenManager::Request> request =
StartTokenRequest();
PerformURLFetchesWithResults(
net::HTTP_OK, GetValidTokenResponse("tokeninfo_access_token", 3600),
net::HTTP_OK, GetValidTokenInfoResponse(kRobotEmail),
net::HTTP_BAD_REQUEST, GetInvalidScopeResponse("test_scope"));
AssertConsumerTokensAndErrors(0, 1);
EXPECT_EQ(consumer_.last_error_.state(),
GoogleServiceAuthError::SCOPE_LIMITED_UNRECOVERABLE_ERROR);
EXPECT_EQ(
consumer_.last_error_.error_message(),
"{ \"error\": \"invalid_scope\", \"error_description\": \"Some requested "
"scopes were invalid. {invalid\\u003d[test_scope}\", \"error_uri\": "
"\"https://developers.google.com/identity/protocols/oauth2\"}");
}
TEST_F(DeviceOAuth2TokenServiceTest,
RefreshTokenValidation_Failure_TokenInfoApiCallHttpError) {
SetUpDefaultValues();
std::unique_ptr<OAuth2AccessTokenManager::Request> request =
StartTokenRequest();
PerformURLFetchesWithResults(
net::HTTP_OK, GetValidTokenResponse("tokeninfo_access_token", 3600),
net::HTTP_INTERNAL_SERVER_ERROR, "", net::HTTP_OK,
GetValidTokenResponse("ignored", 3600));
AssertConsumerTokensAndErrors(0, 1);
}
TEST_F(DeviceOAuth2TokenServiceTest,
RefreshTokenValidation_Failure_TokenInfoApiCallInvalidResponse) {
SetUpDefaultValues();
std::unique_ptr<OAuth2AccessTokenManager::Request> request =
StartTokenRequest();
PerformURLFetchesWithResults(
net::HTTP_OK, GetValidTokenResponse("tokeninfo_access_token", 3600),
net::HTTP_OK, "invalid response", net::HTTP_OK,
GetValidTokenResponse("ignored", 3600));
AssertConsumerTokensAndErrors(0, 1);
}
TEST_F(DeviceOAuth2TokenServiceTest,
RefreshTokenValidation_Failure_CloudPrintAccessTokenHttpError) {
SetUpDefaultValues();
std::unique_ptr<OAuth2AccessTokenManager::Request> request =
StartTokenRequest();
PerformURLFetchesWithResults(
net::HTTP_OK, GetValidTokenResponse("tokeninfo_access_token", 3600),
net::HTTP_OK, GetValidTokenInfoResponse(kRobotEmail),
net::HTTP_BAD_REQUEST, "");
AssertConsumerTokensAndErrors(0, 1);
}
TEST_F(DeviceOAuth2TokenServiceTest,
RefreshTokenValidation_Failure_CloudPrintAccessTokenInvalidResponse) {
SetUpDefaultValues();
std::unique_ptr<OAuth2AccessTokenManager::Request> request =
StartTokenRequest();
PerformURLFetchesWithResults(
net::HTTP_OK, GetValidTokenResponse("tokeninfo_access_token", 3600),
net::HTTP_OK, GetValidTokenInfoResponse(kRobotEmail), net::HTTP_OK,
"invalid request");
AssertConsumerTokensAndErrors(0, 1);
}
TEST_F(DeviceOAuth2TokenServiceTest, RefreshTokenValidation_Failure_BadOwner) {
SetUpDefaultValues();
std::unique_ptr<OAuth2AccessTokenManager::Request> request =
StartTokenRequest();
SetRobotAccountId(kWrongRobotEmail);
PerformURLFetchesWithResults(
net::HTTP_OK, GetValidTokenResponse("tokeninfo_access_token", 3600),
net::HTTP_OK, GetValidTokenInfoResponse(kRobotEmail), net::HTTP_OK,
GetValidTokenResponse("ignored", 3600));
AssertConsumerTokensAndErrors(0, 1);
}
TEST_F(DeviceOAuth2TokenServiceTest, RefreshTokenValidation_Retry) {
SetUpDefaultValues();
std::unique_ptr<OAuth2AccessTokenManager::Request> request =
StartTokenRequest();
PerformURLFetchesWithResults(
net::HTTP_INTERNAL_SERVER_ERROR, "", net::HTTP_OK,
GetValidTokenInfoResponse(kRobotEmail), net::HTTP_OK,
GetValidTokenResponse("ignored", 3600));
AssertConsumerTokensAndErrors(0, 1);
// Retry should succeed.
request = StartTokenRequest();
PerformURLFetches();
AssertConsumerTokensAndErrors(1, 1);
}