Save encrypted auth token in profile prefs. (#32596)

* Save encrypted auth token in profile prefs.
* Using async os crypt.
This commit is contained in:
Pavel Beloborodov
2025-12-09 04:53:01 +01:00
committed by GitHub
parent b050d06cd9
commit b4fc4553c8
18 changed files with 661 additions and 73 deletions
+3
View File
@@ -45,6 +45,7 @@
#include "brave/components/containers/buildflags/buildflags.h"
#include "brave/components/de_amp/common/pref_names.h"
#include "brave/components/debounce/core/browser/debounce_service.h"
#include "brave/components/email_aliases/email_aliases_service.h"
#include "brave/components/global_privacy_control/pref_names.h"
#include "brave/components/ipfs/ipfs_prefs.h"
#include "brave/components/ntp_background_images/browser/view_counter_service.h"
@@ -571,6 +572,8 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) {
containers::RegisterProfilePrefs(registry);
#endif
email_aliases::EmailAliasesService::RegisterProfilePrefs(registry);
OverrideDefaultPrefValues(registry);
}
+3
View File
@@ -15,8 +15,10 @@ static_library("email_aliases") {
"//base",
"//brave/components/email_aliases:features",
"//brave/components/email_aliases:service",
"//chrome/browser:browser_process",
"//chrome/browser/profiles:profile",
"//components/keyed_service/content",
"//components/user_prefs",
"//content/public/browser",
"//mojo/public/cpp/bindings",
]
@@ -54,6 +56,7 @@ if (toolkit_views) {
"//brave/components/email_aliases:email_aliases_api_types",
"//brave/components/email_aliases:features",
"//brave/components/email_aliases:service",
"//brave/components/email_aliases:test_utils",
"//chrome/browser",
"//chrome/browser/ui/views/bubble",
"//chrome/test:test_support",
@@ -7,7 +7,6 @@
#include "base/json/json_reader.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/test/run_until.h"
#include "base/time/time.h"
#include "brave/browser/email_aliases/email_aliases_service_factory.h"
@@ -15,16 +14,19 @@
#include "brave/browser/ui/webui/brave_settings_ui.h"
#include "brave/components/constants/brave_paths.h"
#include "brave/components/email_aliases/email_aliases_api.h"
#include "brave/components/email_aliases/email_aliases_auth.h"
#include "brave/components/email_aliases/email_aliases_service.h"
#include "brave/components/email_aliases/features.h"
#include "brave/components/email_aliases/test_utils.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu_browsertest_util.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu_test_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/views/bubble/webui_bubble_manager.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/render_frame_host.h"
@@ -398,11 +400,13 @@ IN_PROC_BROWSER_TEST_F(EmailAliasesBrowserTest, ContextMenuNotAuthorized) {
}
IN_PROC_BROWSER_TEST_F(EmailAliasesBrowserTest, ContextMenuAuthorized) {
email_aliases_service()->RequestAuthentication("success@domain.com",
base::DoNothing());
ASSERT_TRUE(base::test::RunUntil([&]() {
return !email_aliases_service()->GetAuthTokenForTesting().empty();
}));
auto* service = email_aliases_service();
{
auto initilized = test::AuthStateObserver::Setup(service, true);
}
service->RequestAuthentication("success@domain.com", base::DoNothing());
ASSERT_TRUE(base::test::RunUntil(
[&]() { return !service->GetAuthTokenForTesting().empty(); }));
Navigate(GURL("https://a.test/email_aliases/inputs.html"));
InjectHelpers(ActiveWebContents());
@@ -499,4 +503,26 @@ IN_PROC_BROWSER_TEST_F(EmailAliasesBrowserTest, ContextMenuAuthorizedCancel) {
EXPECT_TRUE(AwaitText("#type-email", "")); // text not changed
}
IN_PROC_BROWSER_TEST_F(EmailAliasesBrowserTest, LogInLogOut) {
// Prepare auth token
EmailAliasesAuth auth(
browser()->profile()->GetPrefs(),
test::GetEncryptor(g_browser_process->os_crypt_async()));
auth.SetAuthEmail(kSuccessEmail);
auth.SetAuthToken("success_token");
// Settings in logged-in state
Navigate(GURL("chrome://settings/email-aliases"));
InjectHelpers(ActiveWebContents());
Wait("#create-new-item-button");
// Reset token
auth.SetAuthToken({});
Wait("#get-login-link-button"); // Settings in sing-in state.
// Logged-in again
auth.SetAuthToken("success_token");
Wait("#create-new-item-button"); // Logged-in state.
}
} // namespace email_aliases
@@ -9,8 +9,10 @@
#include "brave/components/email_aliases/email_aliases_service.h"
#include "brave/components/email_aliases/features.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_selections.h"
#include "components/user_prefs/user_prefs.h"
#include "content/public/browser/storage_partition.h"
namespace email_aliases {
@@ -53,7 +55,8 @@ EmailAliasesServiceFactory::BuildServiceInstanceForBrowserContext(
content::BrowserContext* context) const {
return std::make_unique<EmailAliasesService>(
context->GetDefaultStoragePartition()
->GetURLLoaderFactoryForBrowserProcess());
->GetURLLoaderFactoryForBrowserProcess(),
user_prefs::UserPrefs::Get(context), g_browser_process->os_crypt_async());
}
} // namespace email_aliases
@@ -25,6 +25,11 @@ class EmailAliasesPage extends HTMLElement {
import('/email_aliases.bundle.js' as any).then(({ mount }) => {
mount(subpage)
})
if (loadTimeData.getBoolean('shouldExposeElementsForTesting')) {
;(window as any).testing = (window as any).testing || {}
;(window as any).testing[`emailAliases`] = this.shadowRoot
}
}
}
+1
View File
@@ -206,6 +206,7 @@ brave_chrome_browser_deps = [
"//brave/components/debounce/content/browser",
"//brave/components/email_aliases:features",
"//brave/components/email_aliases:mojom",
"//brave/components/email_aliases:service",
"//brave/components/global_privacy_control",
"//brave/components/google_sign_in_permission",
"//brave/components/https_upgrade_exceptions/browser",
+39 -2
View File
@@ -33,6 +33,20 @@ static_library("features") {
public_deps = [ "//base" ]
}
source_set("auth") {
sources = [
"email_aliases_auth.cc",
"email_aliases_auth.h",
]
deps = [
"//base",
"//components/os_crypt/async/common",
"//components/prefs",
"//services/preferences/public/cpp",
]
}
static_library("service") {
sources = [
"email_aliases_service.cc",
@@ -47,22 +61,45 @@ static_library("service") {
"//brave/components/email_aliases:features",
"//brave/components/resources:strings_grit",
"//components/keyed_service/core",
"//components/os_crypt/async/browser",
"//components/os_crypt/async/common",
"//mojo/public/cpp/bindings",
"//net",
"//services/network/public/cpp",
"//ui/base",
]
public_deps = [ ":auth" ]
}
source_set("test_utils") {
testonly = true
sources = [
"test_utils.cc",
"test_utils.h",
]
deps = [
":service",
"//base/test:test_support",
"//brave/components/email_aliases:mojom",
"//components/os_crypt/async/browser",
"//mojo/public/cpp/bindings",
]
}
source_set("unit_tests") {
testonly = true
sources = [ "email_aliases_service_unittest.cc" ]
deps = [
":features",
":service",
":test_utils",
"//base/test:test_support",
"//brave/components/constants",
"//brave/components/email_aliases:features",
"//brave/components/email_aliases:service",
"//brave/components/resources:strings_grit",
"//components/os_crypt/async/browser:test_support",
"//components/prefs:test_support",
"//services/network:test_support",
"//services/network/public/cpp",
+9
View File
@@ -2,6 +2,15 @@ include_rules = [
"+absl/strings/str_format.h",
"+components/keyed_service/core",
"+components/grit/brave_components_strings.h",
"+components/os_crypt/async",
"+components/prefs",
"+services/network/public/cpp",
"+services/network/test",
"+services/preferences/public/cpp",
]
specific_include_rules = {
"test_utils.*": [
"+gtest"
],
}
@@ -0,0 +1,152 @@
/* Copyright (c) 2025 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#include "brave/components/email_aliases/email_aliases_auth.h"
#include "base/auto_reset.h"
#include "base/base64.h"
#include "base/values.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "services/preferences/public/cpp/dictionary_value_update.h"
#include "services/preferences/public/cpp/scoped_pref_update.h"
namespace {
constexpr char kEmailField[] = "email";
constexpr char kTokenField[] = "token";
bool Encrypt(const os_crypt_async::Encryptor& encryptor,
const std::string& plain_text,
std::string& out) {
if (plain_text.empty()) {
return false;
}
auto encrypted = encryptor.EncryptString(plain_text);
if (!encrypted) {
return false;
}
out = base::Base64Encode(encrypted.value());
return true;
}
bool Decrypt(const os_crypt_async::Encryptor& encryptor,
const std::string& base64,
std::string& out) {
if (base64.empty()) {
return false;
}
auto encrypted = base::Base64Decode(base64);
if (!encrypted) {
return false;
}
auto decrypted = encryptor.DecryptData(encrypted.value());
if (!decrypted) {
return false;
}
out = std::string(decrypted->begin(), decrypted->end());
return true;
}
} // namespace
namespace email_aliases {
EmailAliasesAuth::EmailAliasesAuth(PrefService* prefs_service,
os_crypt_async::Encryptor encryptor,
OnChangedCallback on_changed)
: prefs_service_(prefs_service),
encryptor_(std::move(encryptor)),
on_changed_(std::move(on_changed)) {
CHECK(prefs_service_);
CHECK(on_changed_);
pref_change_registrar_.Init(prefs_service_);
pref_change_registrar_.Add(
prefs::kAuth, base::BindRepeating(&EmailAliasesAuth::OnPrefChanged,
base::Unretained(this)));
auth_email_ = GetAuthEmail();
is_authenticated_ = !CheckAndGetAuthToken().empty() && !auth_email_.empty();
}
EmailAliasesAuth::~EmailAliasesAuth() = default;
// static
void EmailAliasesAuth::RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterDictionaryPref(prefs::kAuth);
}
bool EmailAliasesAuth::IsAuthenticated() const {
return is_authenticated_;
}
void EmailAliasesAuth::SetAuthEmail(const std::string& email) {
if (GetAuthEmail() != email) {
::prefs::ScopedDictionaryPrefUpdate update(prefs_service_, prefs::kAuth);
update->SetString(kEmailField, email);
update->SetString(kTokenField, std::string_view{});
}
}
void EmailAliasesAuth::SetAuthToken(const std::string& auth_token) {
::prefs::ScopedDictionaryPrefUpdate update(prefs_service_, prefs::kAuth);
std::string encrypted;
if (auth_token.empty() || !Encrypt(encryptor_, auth_token, encrypted)) {
update->SetString(kTokenField, std::string_view{});
} else {
update->SetString(kTokenField, encrypted);
}
}
std::string EmailAliasesAuth::GetAuthEmail() const {
const base::Value::Dict& auth = prefs_service_->GetDict(prefs::kAuth);
if (const auto* email = auth.FindString(kEmailField)) {
return *email;
}
return {};
}
std::string EmailAliasesAuth::CheckAndGetAuthToken() {
const base::Value::Dict& auth = prefs_service_->GetDict(prefs::kAuth);
if (const auto* encrypted_token = auth.FindString(kTokenField)) {
if (encrypted_token->empty()) {
return {};
}
std::string token;
if (!Decrypt(encryptor_, *encrypted_token, token)) {
// Failed to decrypt token -> reset.
SetAuthToken({});
return {};
}
return token;
}
return {};
}
void EmailAliasesAuth::OnPrefChanged(const std::string& pref_name) {
if (!notify_) {
return;
}
base::AutoReset reenter(&notify_, false);
auto auth_email = GetAuthEmail();
if (auth_email != auth_email_) {
SetAuthToken({});
auth_email_ = std::move(auth_email);
}
is_authenticated_ = !CheckAndGetAuthToken().empty() && !auth_email_.empty();
on_changed_.Run();
}
} // namespace email_aliases
@@ -0,0 +1,62 @@
/* Copyright (c) 2025 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#ifndef BRAVE_COMPONENTS_EMAIL_ALIASES_EMAIL_ALIASES_AUTH_H_
#define BRAVE_COMPONENTS_EMAIL_ALIASES_EMAIL_ALIASES_AUTH_H_
#include "components/os_crypt/async/common/encryptor.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_member.h"
class PrefRegistrySimple;
class PrefService;
namespace email_aliases {
namespace prefs {
inline constexpr char kAuth[] = "brave.email_aliases.auth";
} // namespace prefs
class EmailAliasesAuth {
public:
using OnChangedCallback = base::RepeatingClosure;
explicit EmailAliasesAuth(PrefService* prefs_service,
os_crypt_async::Encryptor encryptor,
OnChangedCallback on_changed = base::DoNothing());
~EmailAliasesAuth();
static void RegisterProfilePrefs(PrefRegistrySimple* registry);
bool IsAuthenticated() const;
void SetAuthEmail(const std::string& email);
void SetAuthToken(const std::string& auth_token);
std::string GetAuthEmail() const;
std::string CheckAndGetAuthToken();
private:
void OnPrefChanged(const std::string& pref_name);
const raw_ptr<PrefService> prefs_service_ = nullptr;
os_crypt_async::Encryptor encryptor_;
PrefChangeRegistrar pref_change_registrar_;
OnChangedCallback on_changed_;
bool notify_ = true;
std::string auth_email_;
bool is_authenticated_ = false;
};
} // namespace email_aliases
#endif // BRAVE_COMPONENTS_EMAIL_ALIASES_EMAIL_ALIASES_AUTH_H_
@@ -23,6 +23,8 @@
#include "brave/components/email_aliases/email_aliases_api.h"
#include "brave/components/email_aliases/features.h"
#include "components/grit/brave_components_strings.h"
#include "components/os_crypt/async/browser/os_crypt_async.h"
#include "components/os_crypt/async/common/encryptor.h"
#include "mojo/public/cpp/bindings/callback_helpers.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
@@ -120,16 +122,28 @@ GURL EmailAliasesService::GetEmailAliasesServiceURL() {
}
EmailAliasesService::EmailAliasesService(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
PrefService* pref_service,
os_crypt_async::OSCryptAsync* os_crypt_async)
: url_loader_factory_(url_loader_factory),
pref_service_(pref_service),
verify_init_url_(GetAccountsServiceVerifyInitURL()),
verify_result_url_(GetAccountsServiceVerifyResultURL()),
email_aliases_service_base_url_(GetEmailAliasesServiceURL()) {
CHECK(base::FeatureList::IsEnabled(email_aliases::features::kEmailAliases));
CHECK(pref_service_);
os_crypt_async->GetInstance(base::BindOnce(
&EmailAliasesService::OnEncryptorReady, weak_factory_.GetWeakPtr()));
}
EmailAliasesService::~EmailAliasesService() = default;
// static
void EmailAliasesService::RegisterProfilePrefs(PrefRegistrySimple* registry) {
EmailAliasesAuth::RegisterProfilePrefs(registry);
}
void EmailAliasesService::Shutdown() {
receivers_.Clear();
observers_.Clear();
@@ -143,24 +157,27 @@ void EmailAliasesService::BindInterface(
void EmailAliasesService::NotifyObserversAuthStateChanged(
mojom::AuthenticationStatus status,
const std::optional<std::string>& error_message) {
const auto email = auth_ ? auth_->GetAuthEmail() : std::string();
for (auto& observer : observers_) {
observer->OnAuthStateChanged(
mojom::AuthState::New(status, auth_email_, error_message));
mojom::AuthState::New(status, email, error_message));
}
}
void EmailAliasesService::ResetVerificationFlow() {
CHECK(auth_);
verification_simple_url_loader_.reset();
session_request_timer_.Stop();
verification_token_.clear();
auth_token_.clear();
auth_->SetAuthToken({});
}
void EmailAliasesService::RequestAuthentication(
const std::string& auth_email,
RequestAuthenticationCallback callback) {
CHECK(auth_);
ResetVerificationFlow();
auth_email_ = auth_email;
auth_->SetAuthEmail(auth_email);
if (auth_email.empty()) {
std::move(callback).Run(base::unexpected(
l10n_util::GetStringUTF8(IDS_EMAIL_ALIASES_ERROR_NO_EMAIL_PROVIDED)));
@@ -198,6 +215,44 @@ void EmailAliasesService::RequestAuthentication(
kMaxResponseLength.InBytesUnsigned());
}
void EmailAliasesService::OnEncryptorReady(
os_crypt_async::Encryptor encryptor) {
CHECK(!auth_);
auth_.emplace(pref_service_.get(), std::move(encryptor),
base::BindRepeating(&EmailAliasesService::OnAuthChanged,
weak_factory_.GetWeakPtr()));
OnAuthChanged();
}
std::string EmailAliasesService::GetAuthEmail() const {
if (!auth_) {
return {};
}
return auth_->GetAuthEmail();
}
std::string EmailAliasesService::GetAuthToken() {
if (!auth_) {
return {};
}
return auth_->CheckAndGetAuthToken();
}
mojom::AuthenticationStatus EmailAliasesService::GetCurrentStatus() {
if (!auth_) {
return mojom::AuthenticationStatus::kStartup;
} else if (IsAuthenticated()) {
return mojom::AuthenticationStatus::kAuthenticated;
} else if (!verification_token_.empty()) {
return mojom::AuthenticationStatus::kAuthenticating;
}
return mojom::AuthenticationStatus::kUnauthenticated;
}
void EmailAliasesService::OnAuthChanged() {
NotifyObserversAuthStateChanged(GetCurrentStatus());
}
void EmailAliasesService::OnRequestAuthenticationResponse(
RequestAuthenticationCallback callback,
std::optional<std::string> response_body) {
@@ -307,9 +362,8 @@ void EmailAliasesService::OnRequestSessionResponse(
return;
}
// Success; set the auth token and notify observers.
auth_token_ = *parsed_session->auth_token;
auth_->SetAuthToken(*parsed_session->auth_token);
session_poll_elapsed_timer_.reset();
NotifyObserversAuthStateChanged(mojom::AuthenticationStatus::kAuthenticated);
// Kick off an initial aliases refresh on successful authentication.
RefreshAliases();
}
@@ -347,10 +401,12 @@ void EmailAliasesService::CancelAuthenticationOrLogout(
void EmailAliasesService::GenerateAlias(GenerateAliasCallback callback) {
base::Value::Dict body_value; // empty JSON object required by the API
auto wrapper = mojo::WrapCallbackWithDefaultInvokeIfNotRun(
std::move(callback), base::unexpected(std::string()));
ApiFetch(email_aliases_service_base_url_,
net::HttpRequestHeaders::kPostMethod, body_value,
base::BindOnce(&EmailAliasesService::OnGenerateAliasResponse,
weak_factory_.GetWeakPtr(), std::move(callback)));
weak_factory_.GetWeakPtr(), std::move(wrapper)));
}
void EmailAliasesService::UpdateAlias(
@@ -366,10 +422,12 @@ void EmailAliasesService::UpdateAlias(
// For now, we only support active aliases.
request.status = "active";
auto body_value = request.ToValue();
auto wrapper = mojo::WrapCallbackWithDefaultInvokeIfNotRun(
std::move(callback), base::unexpected(std::string()));
ApiFetch(email_aliases_service_base_url_, net::HttpRequestHeaders::kPutMethod,
body_value,
base::BindOnce(&EmailAliasesService::OnEditAliasResponse,
weak_factory_.GetWeakPtr(), std::move(callback),
weak_factory_.GetWeakPtr(), std::move(wrapper),
/*update_expected=*/true));
}
@@ -378,10 +436,12 @@ void EmailAliasesService::DeleteAlias(const std::string& alias_email,
DeleteAliasRequest request;
request.alias = alias_email;
auto body_value = request.ToValue();
auto wrapper = mojo::WrapCallbackWithDefaultInvokeIfNotRun(
std::move(callback), base::unexpected(std::string()));
ApiFetch(email_aliases_service_base_url_,
net::HttpRequestHeaders::kDeleteMethod, body_value,
base::BindOnce(&EmailAliasesService::OnEditAliasResponse,
weak_factory_.GetWeakPtr(), std::move(callback),
weak_factory_.GetWeakPtr(), std::move(wrapper),
/*update_expected=*/false));
}
@@ -391,17 +451,17 @@ void EmailAliasesService::AddObserver(
auto* remote = observers_.Get(id);
if (remote) {
remote->OnAuthStateChanged(
mojom::AuthState::New(mojom::AuthenticationStatus::kUnauthenticated,
/*email=*/"", /*error_message=*/std::nullopt));
mojom::AuthState::New(GetCurrentStatus(), GetAuthEmail(),
/*error_message=*/std::nullopt));
}
}
bool EmailAliasesService::IsAuthenticated() const {
return !auth_email_.empty() && !auth_token_.empty();
return auth_ && auth_->IsAuthenticated();
}
std::string EmailAliasesService::GetAuthTokenForTesting() const {
return auth_token_;
std::string EmailAliasesService::GetAuthTokenForTesting() {
return GetAuthToken();
}
void EmailAliasesService::ApiFetch(const GURL& url,
@@ -430,11 +490,16 @@ void EmailAliasesService::ApiFetchInternal(
const std::string_view method,
std::optional<std::string> serialized_body,
BodyAsStringCallback callback) {
const auto auth_token = GetAuthToken();
if (auth_token.empty()) {
return;
}
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = url;
resource_request->method = method;
resource_request->headers.SetHeader("Authorization",
std::string("Bearer ") + auth_token_);
std::string("Bearer ") + auth_token);
resource_request->headers.SetHeader("X-API-key",
BUILDFLAG(BRAVE_SERVICES_KEY));
auto simple_url_loader = network::SimpleURLLoader::Create(
@@ -17,6 +17,7 @@
#include "base/timer/timer.h"
#include "base/values.h"
#include "brave/components/email_aliases/email_aliases.mojom.h"
#include "brave/components/email_aliases/email_aliases_auth.h"
#include "components/keyed_service/core/keyed_service.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
@@ -24,6 +25,11 @@
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "url/gurl.h"
namespace os_crypt_async {
class Encryptor;
class OSCryptAsync;
} // namespace os_crypt_async
namespace network {
class SharedURLLoaderFactory;
class SimpleURLLoader;
@@ -42,9 +48,13 @@ class EmailAliasesService : public KeyedService,
public mojom::EmailAliasesService {
public:
EmailAliasesService(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory);
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
PrefService* pref_service,
os_crypt_async::OSCryptAsync* os_crypt_async);
~EmailAliasesService() override;
static void RegisterProfilePrefs(PrefRegistrySimple* registry);
// KeyedService:
// Called when the owning profile context is shutting down. Releases
// interface bindings and observers.
@@ -88,7 +98,7 @@ class EmailAliasesService : public KeyedService,
mojo::PendingReceiver<mojom::EmailAliasesService> receiver);
// Returns the current auth token for tests. Empty when unauthenticated.
std::string GetAuthTokenForTesting() const;
std::string GetAuthTokenForTesting();
// Build the fully-qualified Brave Accounts verification URLs.
static GURL GetAccountsServiceVerifyInitURL();
@@ -102,6 +112,15 @@ class EmailAliasesService : public KeyedService,
using BodyAsStringCallback =
base::OnceCallback<void(std::optional<std::string> response_body)>;
void OnEncryptorReady(os_crypt_async::Encryptor encryptor);
std::string GetAuthEmail() const;
std::string GetAuthToken();
mojom::AuthenticationStatus GetCurrentStatus();
void OnAuthChanged();
// Handles the response to the verify/init request. Parses a verification
// token and, if present, proceeds to poll the session endpoint. Invokes
// |callback| with an optional error message.
@@ -176,15 +195,13 @@ class EmailAliasesService : public KeyedService,
// Temporary token returned by verify/init and used to authorize polling.
std::string verification_token_;
// Long-lived token returned by verify/result upon successful authentication.
std::string auth_token_;
// The email address used for the current authentication attempt.
std::string auth_email_;
std::optional<EmailAliasesAuth> auth_;
// URL loader factory used to issue network requests to Brave Accounts.
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
const raw_ptr<PrefService> pref_service_ = nullptr;
// Single SimpleURLLoader instance used for both verify/init and
// verify/result requests. Recreated for each new request.
std::unique_ptr<network::SimpleURLLoader> verification_simple_url_loader_;
@@ -20,7 +20,10 @@
#include "base/types/expected.h"
#include "brave/components/constants/brave_services_key.h"
#include "brave/components/email_aliases/features.h"
#include "brave/components/email_aliases/test_utils.h"
#include "components/grit/brave_components_strings.h"
#include "components/os_crypt/async/browser/test_utils.h"
#include "components/prefs/testing_pref_service.h"
#include "net/base/net_errors.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
@@ -34,45 +37,22 @@ namespace email_aliases {
using AuthenticationStatus = email_aliases::mojom::AuthenticationStatus;
namespace {
// Test observer for authentication state changes
class TestObserver : public email_aliases::mojom::EmailAliasesServiceObserver {
public:
void OnAuthStateChanged(email_aliases::mojom::AuthStatePtr state) override {
last_state = state->status;
}
bool WaitFor(AuthenticationStatus expected) {
return base::test::RunUntil([&]() { return last_state == expected; });
}
void OnAliasesUpdated(std::vector<email_aliases::mojom::AliasPtr>) override {}
AuthenticationStatus last_state = AuthenticationStatus::kUnauthenticated;
mojo::Receiver<email_aliases::mojom::EmailAliasesServiceObserver> receiver_{
this};
void BindReceiver(
mojo::PendingReceiver<email_aliases::mojom::EmailAliasesServiceObserver>
pending) {
receiver_.Bind(std::move(pending));
}
};
} // namespace
class EmailAliasesServiceTest : public ::testing::Test {
protected:
EmailAliasesServiceTest() {
feature_list_.InitAndEnableFeature(email_aliases::features::kEmailAliases);
EmailAliasesService::RegisterProfilePrefs(prefs_.registry());
}
void SetUp() override {
os_crypt_ = os_crypt_async::GetTestOSCryptAsyncForTesting();
keyed_service_ = std::make_unique<EmailAliasesService>(
test_url_loader_factory_.GetSafeWeakWrapper());
test_url_loader_factory_.GetSafeWeakWrapper(), &prefs_,
os_crypt_.get());
keyed_service_->BindInterface(service_.BindNewPipeAndPassReceiver());
observer_ = std::make_unique<TestObserver>();
mojo::PendingRemote<email_aliases::mojom::EmailAliasesServiceObserver>
remote;
observer_->BindReceiver(remote.InitWithNewPipeAndPassReceiver());
service_->AddObserver(std::move(remote));
observer_ = email_aliases::test::AuthStateObserver::Setup(
keyed_service_.get(), true);
}
// Make authentication request and wait for the response.
@@ -135,10 +115,12 @@ class EmailAliasesServiceTest : public ::testing::Test {
base::test::ScopedFeatureList feature_list_;
network::TestURLLoaderFactory test_url_loader_factory_;
TestingPrefServiceSimple prefs_;
std::unique_ptr<os_crypt_async::OSCryptAsync> os_crypt_;
std::unique_ptr<EmailAliasesService> keyed_service_;
mojo::Remote<mojom::EmailAliasesService> service_;
base::test::TaskEnvironment task_environment_;
std::unique_ptr<TestObserver> observer_;
std::unique_ptr<test::AuthStateObserver> observer_;
};
TEST_F(EmailAliasesServiceTest, RequestAuthentication_EmptyEmail) {
@@ -185,6 +167,57 @@ TEST_F(EmailAliasesServiceTest, RequestAuthentication_Success_MultipleCalls) {
EXPECT_TRUE(result3.Get().has_value());
}
TEST_F(EmailAliasesServiceTest, Auth) {
EmailAliasesAuth auth(&prefs_, test::GetEncryptor(os_crypt_.get()),
base::BindLambdaForTesting([&]() {}));
auth.SetAuthEmail("test@domain.com");
EXPECT_EQ(auth.GetAuthEmail(), "test@domain.com");
auth.SetAuthToken("token");
EXPECT_EQ(auth.CheckAndGetAuthToken(), "token");
auth.SetAuthEmail({});
EXPECT_EQ(auth.GetAuthEmail(), "");
EXPECT_EQ(auth.CheckAndGetAuthToken(), "");
auth.SetAuthEmail("test@domain.com");
auth.SetAuthToken("token");
{
// set the same email
prefs_.SetDict(
prefs::kAuth,
base::Value::Dict()
.Set("email", "test@domain.com")
.Set("token", *prefs_.GetDict(prefs::kAuth).FindString("token")));
EXPECT_EQ(auth.GetAuthEmail(), "test@domain.com");
EXPECT_EQ(auth.CheckAndGetAuthToken(), "token");
}
{
// set new email
prefs_.SetDict(
prefs::kAuth,
base::Value::Dict()
.Set("email", "new@domain.com")
.Set("token", *prefs_.GetDict(prefs::kAuth).FindString("token")));
EXPECT_EQ(auth.GetAuthEmail(), "new@domain.com");
EXPECT_EQ(auth.CheckAndGetAuthToken(), ""); // token reset
}
{
// token becomes invalid
auth.SetAuthToken("token");
EXPECT_EQ(auth.GetAuthEmail(), "new@domain.com");
EXPECT_EQ(auth.CheckAndGetAuthToken(), "token");
prefs_.SetDict(prefs::kAuth, base::Value::Dict()
.Set("email", "new@domain.com")
.Set("token", "invalid"));
EXPECT_EQ(auth.GetAuthEmail(), "new@domain.com");
EXPECT_EQ(auth.CheckAndGetAuthToken(), ""); // token reset
}
}
TEST_F(EmailAliasesServiceTest, RequestSession_Success) {
RunRequestSessionTest({"{\"authToken\":\"auth456\", \"verified\":true, "
"\"service\":\"email-aliases\"}"},
@@ -192,6 +225,35 @@ TEST_F(EmailAliasesServiceTest, RequestSession_Success) {
EXPECT_EQ(keyed_service_->GetAuthTokenForTesting(), "auth456");
}
TEST_F(EmailAliasesServiceTest, SessionPreserved) {
RunRequestSessionTest({"{\"authToken\":\"auth456\", \"verified\":true, "
"\"service\":\"email-aliases\"}"},
AuthenticationStatus::kAuthenticated);
// Simulate next start.
keyed_service_ = std::make_unique<EmailAliasesService>(
test_url_loader_factory_.GetSafeWeakWrapper(), &prefs_, os_crypt_.get());
{
auto initialized =
test::AuthStateObserver::Setup(keyed_service_.get(), true);
}
EXPECT_TRUE(keyed_service_->IsAuthenticated());
EXPECT_EQ("auth456", keyed_service_->GetAuthTokenForTesting());
// New Observer is notified.
auto observer = test::AuthStateObserver::Setup(keyed_service_.get());
EXPECT_TRUE(observer->WaitFor(AuthenticationStatus::kAuthenticated));
// Prefs contain values.
EmailAliasesAuth auth(&prefs_, test::GetEncryptor(os_crypt_.get()));
EXPECT_EQ("test@example.com", auth.GetAuthEmail());
EXPECT_EQ("auth456", auth.CheckAndGetAuthToken());
const auto& pref_value = prefs_.GetDict(prefs::kAuth);
EXPECT_EQ("test@example.com", *pref_value.FindString("email"));
EXPECT_FALSE(pref_value.FindString("token")->empty()); // token saved
EXPECT_NE("auth456", *pref_value.FindString("token")); // token encrypted
}
TEST_F(EmailAliasesServiceTest, RequestSession_InvalidJson) {
RunRequestSessionTest({"not a json"}, AuthenticationStatus::kAuthenticating);
}
@@ -207,7 +269,7 @@ TEST_F(EmailAliasesServiceTest, RequestSession_RetryOnMissingAuthToken) {
email_aliases::mojom::AuthenticationStatus::kAuthenticated);
EXPECT_EQ(keyed_service_->GetAuthTokenForTesting(), "auth456");
// unauthenticated, authenticating, authenticated
EXPECT_EQ(observer_->last_state,
EXPECT_EQ(observer_->GetStatus().status,
email_aliases::mojom::AuthenticationStatus::kAuthenticated);
}
@@ -251,13 +313,12 @@ class EmailAliasesServiceTimingTest : public ::testing::Test {
protected:
void SetUp() override {
feature_list_.InitAndEnableFeature(email_aliases::features::kEmailAliases);
EmailAliasesService::RegisterProfilePrefs(prefs_.registry());
os_crypt_ = os_crypt_async::GetTestOSCryptAsyncForTesting();
service_ = std::make_unique<EmailAliasesService>(
url_loader_factory_.GetSafeWeakWrapper());
observer_ = std::make_unique<TestObserver>();
mojo::PendingRemote<email_aliases::mojom::EmailAliasesServiceObserver>
remote;
observer_->BindReceiver(remote.InitWithNewPipeAndPassReceiver());
service_->AddObserver(std::move(remote));
url_loader_factory_.GetSafeWeakWrapper(), &prefs_, os_crypt_.get());
observer_ =
email_aliases::test::AuthStateObserver::Setup(service_.get(), true);
}
// Starts auth and captures verify/result request times via interceptor.
@@ -290,8 +351,10 @@ class EmailAliasesServiceTimingTest : public ::testing::Test {
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
base::test::ScopedFeatureList feature_list_;
network::TestURLLoaderFactory url_loader_factory_;
std::unique_ptr<os_crypt_async::OSCryptAsync> os_crypt_;
TestingPrefServiceSimple prefs_;
std::unique_ptr<EmailAliasesService> service_;
std::unique_ptr<TestObserver> observer_;
std::unique_ptr<test::AuthStateObserver> observer_;
std::vector<base::TimeTicks> verify_result_request_times_;
};
@@ -335,7 +398,7 @@ TEST_F(EmailAliasesServiceTimingTest, VerifyResult_StopsAfterMaxDuration) {
EXPECT_EQ(verify_result_request_times_.size(), expected_requests);
EXPECT_EQ(observer_->last_state,
EXPECT_EQ(observer_->GetStatus().status,
email_aliases::mojom::AuthenticationStatus::kUnauthenticated);
}
@@ -441,10 +504,25 @@ class EmailAliasesAPITest : public ::testing::Test {
return result_out;
}
void SetupAuth(bool auth = true) {
EmailAliasesAuth settings(&prefs_, test::GetEncryptor(os_crypt_.get()));
settings.SetAuthEmail("test@example.com");
if (auth) {
settings.SetAuthToken("token456");
} else {
settings.SetAuthEmail({});
}
}
protected:
void SetUp() override {
EmailAliasesService::RegisterProfilePrefs(prefs_.registry());
os_crypt_ = os_crypt_async::GetTestOSCryptAsyncForTesting();
SetupAuth();
service_ = std::make_unique<EmailAliasesService>(
url_loader_factory_.GetSafeWeakWrapper());
url_loader_factory_.GetSafeWeakWrapper(), &prefs_, os_crypt_.get());
email_aliases::test::AuthStateObserver::Setup(service_.get(), true);
mojo::PendingRemote<mojom::EmailAliasesServiceObserver> remote;
observer_.BindReceiver(remote.InitWithNewPipeAndPassReceiver());
@@ -454,6 +532,8 @@ class EmailAliasesAPITest : public ::testing::Test {
base::test::ScopedFeatureList feature_list_{features::kEmailAliases};
base::test::TaskEnvironment task_environment_;
network::TestURLLoaderFactory url_loader_factory_;
std::unique_ptr<os_crypt_async::OSCryptAsync> os_crypt_;
TestingPrefServiceSimple prefs_;
std::unique_ptr<EmailAliasesService> service_;
AliasObserver observer_;
};
@@ -62,6 +62,7 @@ export const ListIntroduction = ({
</Description>
</Col>
<Button
id='create-new-item-button'
isDisabled={aliasesCount >= MAX_ALIASES}
kind='filled'
size='small'
@@ -83,6 +83,7 @@ const BeforeSendingEmailForm = ({
value={email}
/>
<Button
id='get-login-link-button'
onClick={requestAuthentication}
type='submit'
kind='filled'
+68
View File
@@ -0,0 +1,68 @@
// Copyright (c) 2025 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
#include "brave/components/email_aliases/test_utils.h"
#include "base/test/run_until.h"
#include "base/test/test_future.h"
#include "components/os_crypt/async/browser/os_crypt_async.h"
#include "gtest/gtest.h"
namespace email_aliases::test {
AuthStateObserver::AuthStateObserver(
mojo::PendingReceiver<email_aliases::mojom::EmailAliasesServiceObserver>
pending) {
receiver_.Bind(std::move(pending));
}
AuthStateObserver::~AuthStateObserver() = default;
// static
std::unique_ptr<AuthStateObserver> AuthStateObserver::Setup(
EmailAliasesService* service,
bool wait_initialized) {
mojo::PendingRemote<email_aliases::mojom::EmailAliasesServiceObserver> remote;
auto observer = base::WrapUnique(
new AuthStateObserver(remote.InitWithNewPipeAndPassReceiver()));
service->AddObserver(std::move(remote));
if (wait_initialized) {
EXPECT_TRUE(observer->WaitInitialized());
}
return observer;
}
const mojom::AuthState& AuthStateObserver::GetStatus() const {
return *last_status_;
}
[[nodiscard]] bool AuthStateObserver::WaitFor(
mojom::AuthenticationStatus status) {
return base::test::RunUntil(
[status, this]() { return GetStatus().status == status; });
}
[[nodiscard]] bool AuthStateObserver::WaitInitialized() {
return base::test::RunUntil([this]() {
return GetStatus().status != mojom::AuthenticationStatus::kStartup;
});
}
void AuthStateObserver::OnAuthStateChanged(
email_aliases::mojom::AuthStatePtr state) {
last_status_ = std::move(state);
}
void AuthStateObserver::OnAliasesUpdated(
std::vector<email_aliases::mojom::AliasPtr>) {}
os_crypt_async::Encryptor GetEncryptor(os_crypt_async::OSCryptAsync* os_crypt) {
base::test::TestFuture<os_crypt_async::Encryptor> result;
os_crypt->GetInstance(result.GetCallback());
return result.Take();
}
} // namespace email_aliases::test
+54
View File
@@ -0,0 +1,54 @@
// Copyright (c) 2025 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
#ifndef BRAVE_COMPONENTS_EMAIL_ALIASES_TEST_UTILS_H_
#define BRAVE_COMPONENTS_EMAIL_ALIASES_TEST_UTILS_H_
#include <memory>
#include "brave/components/email_aliases/email_aliases.mojom.h"
#include "brave/components/email_aliases/email_aliases_auth.h"
#include "brave/components/email_aliases/email_aliases_service.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
namespace email_aliases::test {
// Test observer for authentication state changes
class AuthStateObserver
: public email_aliases::mojom::EmailAliasesServiceObserver {
public:
~AuthStateObserver() override;
static std::unique_ptr<AuthStateObserver> Setup(
EmailAliasesService* service,
bool wait_initialized = false);
const mojom::AuthState& GetStatus() const;
[[nodiscard]] bool WaitFor(mojom::AuthenticationStatus status);
[[nodiscard]] bool WaitInitialized();
private:
explicit AuthStateObserver(
mojo::PendingReceiver<email_aliases::mojom::EmailAliasesServiceObserver>
pending);
void OnAuthStateChanged(email_aliases::mojom::AuthStatePtr state) override;
void OnAliasesUpdated(std::vector<email_aliases::mojom::AliasPtr>) override;
mojom::AuthStatePtr last_status_ =
mojom::AuthState::New(mojom::AuthenticationStatus::kStartup,
std::string(),
std::nullopt);
mojo::Receiver<email_aliases::mojom::EmailAliasesServiceObserver> receiver_{
this};
};
os_crypt_async::Encryptor GetEncryptor(os_crypt_async::OSCryptAsync* os_crypt);
} // namespace email_aliases::test
#endif // BRAVE_COMPONENTS_EMAIL_ALIASES_TEST_UTILS_H_
+1
View File
@@ -10,6 +10,7 @@ brave_components_os_crypt_visibility = [
"//brave/components/brave_account",
"//brave/components/brave_rewards/content",
"//brave/components/brave_sync:prefs",
"//brave/components/email_aliases:auth",
"//brave/components/sync/service:unit_tests",
"//chrome/utility",
"//components/os_crypt/sync",