0

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:
Dustin J. Mitchell
2023-06-21 14:12:57 +00:00
committed by Chromium LUCI CQ
parent a8cdcd14e1
commit 84c6475e0d
5 changed files with 303 additions and 39 deletions

@ -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",