Support fetching tokens in IpProtectionAuthTokenGetter
With this change, `IpProtectionAuthTokenGetter` supports fetching batches of blind-signed auth tokens. Bug: 1444621 Change-Id: I0be2cd594b3dbe506d0bd7fbbddc0ac88970a6cc Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4594925 Reviewed-by: John Abd-El-Malek <jam@chromium.org> Reviewed-by: Andrew Williams <awillia@chromium.org> Commit-Queue: Dustin Mitchell <djmitche@chromium.org> Auto-Submit: Dustin Mitchell <djmitche@chromium.org> Reviewed-by: Brianna Goldstein <brgoldstein@google.com> Cr-Commit-Position: refs/heads/main@{#1160612}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
a8cdcd14e1
commit
84c6475e0d
@ -958,7 +958,11 @@ _BANNED_CPP_FUNCTIONS : Sequence[BanRule] = (
|
||||
'API.',
|
||||
),
|
||||
True,
|
||||
[_THIRD_PARTY_EXCEPT_BLINK], # Don't warn in third_party folders.
|
||||
[
|
||||
# Needed to use QUICHE API.
|
||||
r'chrome/browser/ip_protection/.*',
|
||||
_THIRD_PARTY_EXCEPT_BLINK
|
||||
], # Don't warn in third_party folders.
|
||||
),
|
||||
BanRule(
|
||||
r'/\bstd::(u16)?string_view\b',
|
||||
|
@ -11,25 +11,38 @@
|
||||
#include "google_apis/google_api_keys.h"
|
||||
|
||||
IpProtectionAuthTokenGetter::IpProtectionAuthTokenGetter(
|
||||
signin::IdentityManager* identity_manager)
|
||||
: identity_manager_(identity_manager) {
|
||||
signin::IdentityManager* identity_manager,
|
||||
quiche::BlindSignAuthInterface* bsa)
|
||||
: identity_manager_(identity_manager), bsa_(bsa) {
|
||||
CHECK(identity_manager);
|
||||
}
|
||||
|
||||
IpProtectionAuthTokenGetter::~IpProtectionAuthTokenGetter() = default;
|
||||
|
||||
void IpProtectionAuthTokenGetter::TryGetAuthToken(
|
||||
TryGetAuthTokenCallback callback) {
|
||||
on_token_recieved_callback_ = std::move(callback);
|
||||
if (!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
|
||||
std::move(on_token_recieved_callback_).Run(absl::nullopt);
|
||||
return;
|
||||
}
|
||||
// static
|
||||
IpProtectionAuthTokenGetter IpProtectionAuthTokenGetter::CreateForTesting(
|
||||
signin::IdentityManager* identity_manager,
|
||||
quiche::BlindSignAuthInterface* bsa) {
|
||||
return IpProtectionAuthTokenGetter(identity_manager, bsa);
|
||||
}
|
||||
|
||||
void IpProtectionAuthTokenGetter::TryGetAuthTokens(
|
||||
int batch_size,
|
||||
TryGetAuthTokensCallback callback) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
CHECK(!try_get_auth_token_callback_)
|
||||
<< "Concurrent calls to TryGetAuthTokens are not allowed";
|
||||
DCHECK(batch_size > 0);
|
||||
try_get_auth_token_callback_ = std::move(callback);
|
||||
batch_size_ = batch_size;
|
||||
RequestOAuthToken();
|
||||
}
|
||||
|
||||
void IpProtectionAuthTokenGetter::RequestOAuthToken() {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
if (!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
|
||||
std::move(try_get_auth_token_callback_).Run(absl::nullopt);
|
||||
return;
|
||||
}
|
||||
|
||||
signin::ScopeSet scopes;
|
||||
scopes.insert(GaiaConstants::kIpProtectionAuthScope);
|
||||
@ -38,18 +51,19 @@ void IpProtectionAuthTokenGetter::RequestOAuthToken() {
|
||||
auto mode =
|
||||
signin::PrimaryAccountAccessTokenFetcher::Mode::kWaitUntilAvailable;
|
||||
|
||||
// Create the OAuth token fetcher and call OnRequestCompleted when
|
||||
// complete.
|
||||
// base::Unretained() is safe since `this` owns `access_token_fetcher_`
|
||||
// Create the OAuth token fetcher and call `OnRequestOAuthTokenCompleted()`
|
||||
// when complete. base::Unretained() is safe since `this` owns
|
||||
// `access_token_fetcher_`
|
||||
access_token_fetcher_ =
|
||||
std::make_unique<signin::PrimaryAccountAccessTokenFetcher>(
|
||||
/*consumer_name=*/"IpProtectionService", identity_manager_, scopes,
|
||||
base::BindOnce(&IpProtectionAuthTokenGetter::OnRequestCompleted,
|
||||
base::Unretained(this)),
|
||||
base::BindOnce(
|
||||
&IpProtectionAuthTokenGetter::OnRequestOAuthTokenCompleted,
|
||||
base::Unretained(this)),
|
||||
mode, signin::ConsentLevel::kSignin);
|
||||
}
|
||||
|
||||
void IpProtectionAuthTokenGetter::OnRequestCompleted(
|
||||
void IpProtectionAuthTokenGetter::OnRequestOAuthTokenCompleted(
|
||||
GoogleServiceAuthError error,
|
||||
signin::AccessTokenInfo access_token_info) {
|
||||
access_token_fetcher_.reset();
|
||||
@ -57,9 +71,32 @@ void IpProtectionAuthTokenGetter::OnRequestCompleted(
|
||||
// If we fail to get an OAuth token don't attempt to fetch from Phosphor as
|
||||
// the request is guaranteed to fail.
|
||||
if (error.state() != GoogleServiceAuthError::NONE) {
|
||||
std::move(on_token_recieved_callback_).Run(absl::nullopt);
|
||||
std::move(try_get_auth_token_callback_).Run(absl::nullopt);
|
||||
return;
|
||||
}
|
||||
|
||||
access_token_ = access_token_info;
|
||||
FetchBlindSignedToken(access_token_info);
|
||||
}
|
||||
|
||||
void IpProtectionAuthTokenGetter::FetchBlindSignedToken(
|
||||
signin::AccessTokenInfo access_token_info) {
|
||||
bsa_->GetTokens(
|
||||
access_token_info.token, batch_size_,
|
||||
[this](absl::StatusOr<absl::Span<quiche::BlindSignToken>> tokens) {
|
||||
OnFetchBlindSignedTokenCompleted(tokens);
|
||||
});
|
||||
}
|
||||
|
||||
void IpProtectionAuthTokenGetter::OnFetchBlindSignedTokenCompleted(
|
||||
absl::StatusOr<absl::Span<quiche::BlindSignToken>> tokens) {
|
||||
if (!tokens.ok() || tokens.value().size() == 0) {
|
||||
std::move(try_get_auth_token_callback_).Run(absl::nullopt);
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::string> result;
|
||||
std::transform(tokens->begin(), tokens->end(), std::back_inserter(result),
|
||||
[](quiche::BlindSignToken token) { return token.token; });
|
||||
|
||||
std::move(try_get_auth_token_callback_).Run(std::move(result));
|
||||
}
|
||||
|
@ -14,42 +14,72 @@
|
||||
#include "components/signin/public/identity_manager/access_token_info.h"
|
||||
#include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h"
|
||||
#include "google_apis/gaia/google_service_auth_error.h"
|
||||
#include "net/third_party/quiche/src/quiche/blind_sign_auth/blind_sign_auth.h"
|
||||
#include "services/network/public/cpp/shared_url_loader_factory.h"
|
||||
|
||||
namespace quiche {
|
||||
class BlindSignAuth;
|
||||
} // namespace quiche
|
||||
|
||||
// Fetches IP protection tokens on demand for the network service.
|
||||
//
|
||||
// This class handles both requesting OAuth2 tokens for the signed-in user, and
|
||||
// fetching blind-signed auth tokens for that user. It may only be used on the
|
||||
// UI thread.
|
||||
class IpProtectionAuthTokenGetter {
|
||||
public:
|
||||
using TryGetAuthTokenCallback =
|
||||
base::OnceCallback<void(const absl::optional<std::string>& ipp_header)>;
|
||||
|
||||
// `identity_manager` must outlive `this`
|
||||
explicit IpProtectionAuthTokenGetter(
|
||||
signin::IdentityManager* identity_manager);
|
||||
using TryGetAuthTokensCallback = base::OnceCallback<void(
|
||||
const absl::optional<std::vector<std::string>>& tokens)>;
|
||||
|
||||
~IpProtectionAuthTokenGetter();
|
||||
|
||||
void TryGetAuthToken(TryGetAuthTokenCallback callback);
|
||||
// Create a getter with the given BSA implementation, typically a mock.
|
||||
static IpProtectionAuthTokenGetter CreateForTesting(
|
||||
signin::IdentityManager* identity_manager,
|
||||
quiche::BlindSignAuthInterface* bsa);
|
||||
|
||||
// Get a batch of blind-signed auth tokens.
|
||||
//
|
||||
// It is forbidden for two calls to this method to be outstanding at the same
|
||||
// time.
|
||||
void TryGetAuthTokens(int batch_size, TryGetAuthTokensCallback callback);
|
||||
|
||||
private:
|
||||
IpProtectionAuthTokenGetter(signin::IdentityManager* identity_manager,
|
||||
quiche::BlindSignAuthInterface* bsa);
|
||||
|
||||
// Calls the IdentityManager asynchronously to request the OAuth token for the
|
||||
// logged in user.
|
||||
void RequestOAuthToken();
|
||||
void OnRequestOAuthTokenCompleted(GoogleServiceAuthError error,
|
||||
signin::AccessTokenInfo access_token_info);
|
||||
|
||||
// Gets the access token and caches the result.
|
||||
void OnRequestCompleted(GoogleServiceAuthError error,
|
||||
signin::AccessTokenInfo access_token_info);
|
||||
|
||||
// `FetchBlindSignedToken` calls into the quiche::BlindSignAuth library to
|
||||
// request an Auth token for use at the IP Protection proxies. Once retrieved,
|
||||
// the method will call the `on_token_recieved_callback_` to send the token
|
||||
// back to the network process.
|
||||
void FetchBlindSignedToken();
|
||||
|
||||
signin::AccessTokenInfo access_token_;
|
||||
// `FetchBlindSignedToken()` calls into the `quiche::BlindSignAuth` library to
|
||||
// request a blind-signed auth token for use at the IP Protection proxies.
|
||||
void FetchBlindSignedToken(signin::AccessTokenInfo access_token_info);
|
||||
void OnFetchBlindSignedTokenCompleted(
|
||||
absl::StatusOr<absl::Span<quiche::BlindSignToken>>);
|
||||
|
||||
// The object used to get an OAuth token. `identity_manager` must outlive
|
||||
// `this`.
|
||||
const raw_ptr<signin::IdentityManager> identity_manager_;
|
||||
|
||||
// The BlindSignAuth implementation used to fetch blind-signed auth tokens.
|
||||
std::unique_ptr<quiche::BlindSignAuth> blind_sign_auth_;
|
||||
|
||||
// For testing, BlindSignAuth is accessed via its interface. In production,
|
||||
// this is the same pointer as `blind_sign_auth_`.
|
||||
raw_ptr<quiche::BlindSignAuthInterface> bsa_;
|
||||
|
||||
// Used by `RequestOAuthToken()`.
|
||||
std::unique_ptr<signin::PrimaryAccountAccessTokenFetcher>
|
||||
access_token_fetcher_;
|
||||
// Used to notify the network process that tokens have been fetched.
|
||||
TryGetAuthTokenCallback on_token_recieved_callback_;
|
||||
|
||||
// The batch size of the current request.
|
||||
int batch_size_ = 0;
|
||||
|
||||
// The callback for the executing `TryGetAuthTokens()` call.
|
||||
TryGetAuthTokensCallback try_get_auth_token_callback_;
|
||||
};
|
||||
|
||||
#endif // CHROME_BROWSER_IP_PROTECTION_IP_PROTECTION_AUTH_TOKEN_GETTER_H_
|
||||
|
@ -0,0 +1,192 @@
|
||||
// Copyright 2023 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/ip_protection/ip_protection_auth_token_getter.h"
|
||||
#include "base/test/test_future.h"
|
||||
#include "components/signin/public/identity_manager/identity_test_environment.h"
|
||||
#include "content/public/browser/browser_thread.h"
|
||||
#include "content/public/test/browser_task_environment.h"
|
||||
#include "testing/gtest/include/gtest/gtest.h"
|
||||
|
||||
namespace {
|
||||
|
||||
class MockBlindSignAuth : public quiche::BlindSignAuthInterface {
|
||||
public:
|
||||
using BlindSignTokenCallback =
|
||||
std::function<void(absl::StatusOr<absl::Span<quiche::BlindSignToken>>)>;
|
||||
void GetTokens(absl::string_view oauth_token,
|
||||
int num_tokens,
|
||||
BlindSignTokenCallback callback) override {
|
||||
get_tokens_called_ = true;
|
||||
oauth_token_ = oauth_token;
|
||||
num_tokens_ = num_tokens;
|
||||
|
||||
absl::StatusOr<absl::Span<quiche::BlindSignToken>> result;
|
||||
if (status_.ok()) {
|
||||
result = absl::Span<quiche::BlindSignToken>(tokens_);
|
||||
} else {
|
||||
result = status_;
|
||||
}
|
||||
|
||||
content::GetUIThreadTaskRunner({})->PostTask(
|
||||
FROM_HERE,
|
||||
base::BindOnce(
|
||||
[](BlindSignTokenCallback callback,
|
||||
absl::StatusOr<absl::Span<quiche::BlindSignToken>> result) {
|
||||
std::move(callback)(std::move(result));
|
||||
},
|
||||
std::move(callback), std::move(result)));
|
||||
}
|
||||
|
||||
// True if `GetTokens()` was called.
|
||||
bool get_tokens_called_;
|
||||
|
||||
// The token with which `GetTokens()` was called.
|
||||
std::string oauth_token_;
|
||||
|
||||
// The num_tokens with which `GetTokens()` was called.
|
||||
int num_tokens_;
|
||||
|
||||
// If not Ok, the status that will be returned from `GetTokens()`.
|
||||
absl::Status status_ = absl::OkStatus();
|
||||
|
||||
// The tokens that will be returned from `GetTokens()` , if `status_` is not
|
||||
// `OkStatus`.
|
||||
std::vector<quiche::BlindSignToken> tokens_;
|
||||
};
|
||||
|
||||
enum class PrimaryAccountBehavior {
|
||||
// Primary account not set.
|
||||
kNone,
|
||||
|
||||
// Primary account exists but returns an error fetching access token.
|
||||
kTokenFetchError,
|
||||
|
||||
// Primary account exists and returns OAuth token "access_token".
|
||||
kExists,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class IpProtectionAuthTokenGetterTest : public testing::Test {
|
||||
protected:
|
||||
using TokensResult = const absl::optional<std::vector<std::string>>;
|
||||
// Get the IdentityManager for this test.
|
||||
signin::IdentityManager* IdentityManager() {
|
||||
return identity_test_env_.identity_manager();
|
||||
}
|
||||
|
||||
// Call `TryGetAuthTokens()` with `TokensCallback()` and run until it
|
||||
// completes.
|
||||
void TryGetAuthTokens(int num_tokens, IpProtectionAuthTokenGetter& getter) {
|
||||
if (primary_account_behavior_ != PrimaryAccountBehavior::kNone) {
|
||||
identity_test_env_.MakePrimaryAccountAvailable(
|
||||
"test@example.com", signin::ConsentLevel::kSignin);
|
||||
}
|
||||
|
||||
getter.TryGetAuthTokens(num_tokens, tokens_future_.GetCallback());
|
||||
|
||||
switch (primary_account_behavior_) {
|
||||
case PrimaryAccountBehavior::kNone:
|
||||
break;
|
||||
case PrimaryAccountBehavior::kTokenFetchError:
|
||||
identity_test_env_
|
||||
.WaitForAccessTokenRequestIfNecessaryAndRespondWithError(
|
||||
GoogleServiceAuthError(
|
||||
GoogleServiceAuthError::CONNECTION_FAILED));
|
||||
break;
|
||||
case PrimaryAccountBehavior::kExists:
|
||||
identity_test_env_
|
||||
.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
|
||||
"access_token", base::Time::Now());
|
||||
break;
|
||||
}
|
||||
ASSERT_TRUE(tokens_future_.Wait()) << "TryGetAuthTokens did not call back";
|
||||
}
|
||||
|
||||
// The behavior of the identity manager.
|
||||
PrimaryAccountBehavior primary_account_behavior_ =
|
||||
PrimaryAccountBehavior::kExists;
|
||||
|
||||
// Run on the UI thread.
|
||||
content::BrowserTaskEnvironment task_environment_;
|
||||
base::test::TestFuture<TokensResult&> tokens_future_;
|
||||
|
||||
// Test environment for IdentityManager. This must come after the
|
||||
// TaskEnvironment.
|
||||
signin::IdentityTestEnvironment identity_test_env_;
|
||||
};
|
||||
|
||||
// The success case: a primary account is available, and BSA gets a token for
|
||||
// it.
|
||||
TEST_F(IpProtectionAuthTokenGetterTest, Success) {
|
||||
primary_account_behavior_ = PrimaryAccountBehavior::kExists;
|
||||
auto bsa = MockBlindSignAuth();
|
||||
auto getter =
|
||||
IpProtectionAuthTokenGetter::CreateForTesting(IdentityManager(), &bsa);
|
||||
bsa.tokens_ = {{"single-use-1", absl::Now()}, {"single-use-2", absl::Now()}};
|
||||
|
||||
TryGetAuthTokens(2, getter);
|
||||
|
||||
EXPECT_TRUE(bsa.get_tokens_called_);
|
||||
EXPECT_EQ(bsa.oauth_token_, "access_token");
|
||||
EXPECT_EQ(bsa.num_tokens_, 2);
|
||||
std::vector<std::string> expected = {"single-use-1", "single-use-2"};
|
||||
EXPECT_EQ(*tokens_future_.Get(), expected);
|
||||
}
|
||||
|
||||
// BSA returns no tokens.
|
||||
TEST_F(IpProtectionAuthTokenGetterTest, NoTokens) {
|
||||
primary_account_behavior_ = PrimaryAccountBehavior::kExists;
|
||||
auto bsa = MockBlindSignAuth();
|
||||
auto getter =
|
||||
IpProtectionAuthTokenGetter::CreateForTesting(IdentityManager(), &bsa);
|
||||
|
||||
TryGetAuthTokens(1, getter);
|
||||
|
||||
EXPECT_TRUE(bsa.get_tokens_called_);
|
||||
EXPECT_EQ(bsa.oauth_token_, "access_token");
|
||||
EXPECT_EQ(tokens_future_.Get(), absl::nullopt);
|
||||
}
|
||||
|
||||
// BSA returns an error.
|
||||
TEST_F(IpProtectionAuthTokenGetterTest, BlindSignedTokenError) {
|
||||
primary_account_behavior_ = PrimaryAccountBehavior::kExists;
|
||||
auto bsa = MockBlindSignAuth();
|
||||
auto getter =
|
||||
IpProtectionAuthTokenGetter::CreateForTesting(IdentityManager(), &bsa);
|
||||
bsa.status_ = absl::NotFoundError("uhoh");
|
||||
|
||||
TryGetAuthTokens(1, getter);
|
||||
|
||||
EXPECT_TRUE(bsa.get_tokens_called_);
|
||||
EXPECT_EQ(bsa.oauth_token_, "access_token");
|
||||
EXPECT_EQ(tokens_future_.Get(), absl::nullopt);
|
||||
}
|
||||
|
||||
// Fetching OAuth token returns an error.
|
||||
TEST_F(IpProtectionAuthTokenGetterTest, AuthTokenError) {
|
||||
primary_account_behavior_ = PrimaryAccountBehavior::kTokenFetchError;
|
||||
auto bsa = MockBlindSignAuth();
|
||||
auto getter =
|
||||
IpProtectionAuthTokenGetter::CreateForTesting(IdentityManager(), &bsa);
|
||||
|
||||
TryGetAuthTokens(1, getter);
|
||||
|
||||
EXPECT_FALSE(bsa.get_tokens_called_);
|
||||
EXPECT_EQ(tokens_future_.Get(), absl::nullopt);
|
||||
}
|
||||
|
||||
// No primary account.
|
||||
TEST_F(IpProtectionAuthTokenGetterTest, NoPrimary) {
|
||||
primary_account_behavior_ = PrimaryAccountBehavior::kNone;
|
||||
auto bsa = MockBlindSignAuth();
|
||||
auto getter =
|
||||
IpProtectionAuthTokenGetter::CreateForTesting(IdentityManager(), &bsa);
|
||||
|
||||
TryGetAuthTokens(1, getter);
|
||||
|
||||
EXPECT_FALSE(bsa.get_tokens_called_);
|
||||
EXPECT_EQ(tokens_future_.Get(), absl::nullopt);
|
||||
}
|
@ -5716,6 +5716,7 @@ test("unit_tests") {
|
||||
"../browser/idle/idle_detection_permission_context_unittest.cc",
|
||||
"../browser/internal_auth_unittest.cc",
|
||||
"../browser/ip_protection/blind_sign_http_impl_unittest.cc",
|
||||
"../browser/ip_protection/ip_protection_auth_token_getter_unittest.cc",
|
||||
"../browser/k_anonymity_service/k_anonymity_service_client_unittest.cc",
|
||||
"../browser/k_anonymity_service/k_anonymity_service_storage_unittest.cc",
|
||||
"../browser/k_anonymity_service/k_anonymity_trust_token_getter_unittest.cc",
|
||||
|
Reference in New Issue
Block a user