Commit 31219d16 authored by William Lin's avatar William Lin Committed by Commit Bot

Allow GetAuthToken to skip the account chooser

Unbundled consent will allow GetAuthToken to make API calls to the
server to automatically select an account to request permissions from.
This account can be one specified by the extension or one that
previously granted permissions to the extension. This can be useful for
users so they don't need to choose or remember the "correct" account in
the account chooser. This CL adds this feature of skipping the account
chooser and puts it behind a flag to enable it.

Bug: 1100535
Change-Id: I859784b5d3a533144c48802845b3317ce0e01935
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2363879
Commit-Queue: William Lin <williamlin@google.com>
Reviewed-by: default avatarDevlin <rdevlin.cronin@chromium.org>
Reviewed-by: default avatarAlex Ilin <alexilin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#800794}
parent b8b0a643
......@@ -226,10 +226,12 @@ class TestOAuth2MintTokenFlow : public OAuth2MintTokenFlow {
};
TestOAuth2MintTokenFlow(ResultType result,
const std::set<std::string>* requested_scopes,
const std::set<std::string>& granted_scopes,
OAuth2MintTokenFlow::Delegate* delegate)
: OAuth2MintTokenFlow(delegate, OAuth2MintTokenFlow::Parameters()),
result_(result),
requested_scopes_(requested_scopes),
granted_scopes_(granted_scopes),
delegate_(delegate) {}
......@@ -247,7 +249,10 @@ class TestOAuth2MintTokenFlow : public OAuth2MintTokenFlow {
break;
}
case MINT_TOKEN_SUCCESS: {
delegate_->OnMintTokenSuccess(kAccessToken, granted_scopes_, 3600);
if (granted_scopes_.empty())
delegate_->OnMintTokenSuccess(kAccessToken, *requested_scopes_, 3600);
else
delegate_->OnMintTokenSuccess(kAccessToken, granted_scopes_, 3600);
break;
}
case MINT_TOKEN_FAILURE: {
......@@ -272,7 +277,8 @@ class TestOAuth2MintTokenFlow : public OAuth2MintTokenFlow {
private:
ResultType result_;
const std::set<std::string>& granted_scopes_;
const std::set<std::string>* requested_scopes_;
std::set<std::string> granted_scopes_;
OAuth2MintTokenFlow::Delegate* delegate_;
};
......@@ -350,15 +356,17 @@ class FakeGetAuthTokenFunction : public IdentityGetAuthTokenFunction {
flow_queue_.push(std::move(flow));
}
void push_mint_token_result(TestOAuth2MintTokenFlow::ResultType result_type) {
void push_mint_token_result(
TestOAuth2MintTokenFlow::ResultType result_type,
const std::set<std::string>& granted_scopes = {}) {
// If `granted_scopes` is empty, `TestOAuth2MintTokenFlow` returns the
// requested scopes (retrieved from `token_key`) in a mint token success
// flow by default. Since the scopes in `token_key` may be populated at a
// later time, the requested scopes cannot be immediately copied, so a
// pointer is passed instead.
const ExtensionTokenKey* token_key = GetExtensionTokenKeyForTest();
push_mint_token_result(result_type, token_key->scopes);
}
void push_mint_token_result(TestOAuth2MintTokenFlow::ResultType result_type,
const std::set<std::string>& granted_scopes) {
push_mint_token_flow(std::make_unique<TestOAuth2MintTokenFlow>(
result_type, granted_scopes, this));
result_type, &token_key->scopes, granted_scopes, this));
}
// Sets scope UI to not complete immediately. Call
......@@ -527,6 +535,10 @@ class FakeGetAuthTokenFunction : public IdentityGetAuthTokenFunction {
return IdentityGetAuthTokenFunction::enable_granular_permissions();
}
std::string GetSelectedUserId() const {
return IdentityGetAuthTokenFunction::GetSelectedUserId();
}
private:
~FakeGetAuthTokenFunction() override {}
bool login_access_token_result_;
......@@ -866,14 +878,27 @@ class GetAuthTokenFunctionTest
: public IdentityTestWithSignin,
public signin::IdentityManager::DiagnosticsObserver {
public:
explicit GetAuthTokenFunctionTest(bool is_return_scopes_enabled = true) {
explicit GetAuthTokenFunctionTest(bool is_return_scopes_enabled = true,
bool is_selected_user_id_enabled = true) {
std::vector<base::Feature> enabled_features;
std::vector<base::Feature> disabled_features;
if (is_return_scopes_enabled) {
feature_list_.InitAndEnableFeature(
enabled_features.push_back(
extensions_features::kReturnScopesInGetAuthToken);
} else {
feature_list_.InitAndDisableFeature(
disabled_features.push_back(
extensions_features::kReturnScopesInGetAuthToken);
}
if (is_selected_user_id_enabled) {
enabled_features.push_back(
extensions_features::kSelectedUserIdInGetAuthToken);
} else {
disabled_features.push_back(
extensions_features::kSelectedUserIdInGetAuthToken);
}
feature_list_.InitWithFeatures(enabled_features, disabled_features);
}
std::string IssueLoginAccessTokenForAccount(const CoreAccountId& account_id) {
......@@ -3175,6 +3200,202 @@ class RemoveCachedAuthTokenFunctionTest : public ExtensionBrowserTest {
}
};
class GetAuthTokenFunctionSelectedUserIdTest : public GetAuthTokenFunctionTest {
public:
explicit GetAuthTokenFunctionSelectedUserIdTest(
bool is_selected_user_id_enabled = true)
: GetAuthTokenFunctionTest(true, is_selected_user_id_enabled) {}
// Executes a new function and checks that the selected_user_id is the
// expected value. The interactive and scopes field are predefined.
// The account id specified by the extension is optional.
void RunNewFunctionAndExpectSelectedUserId(
const scoped_refptr<const extensions::Extension>& extension,
const std::string& expected_selected_user_id,
const base::Optional<std::string> requested_account = base::nullopt) {
auto func = base::MakeRefCounted<FakeGetAuthTokenFunction>();
func->set_extension(extension);
RunFunctionAndExpectSelectedUserId(func, expected_selected_user_id,
requested_account);
}
void RunFunctionAndExpectSelectedUserId(
const scoped_refptr<FakeGetAuthTokenFunction>& func,
const std::string& expected_selected_user_id,
const base::Optional<std::string> requested_account = base::nullopt) {
// Stops the function right before selected_user_id would be used.
MockQueuedMintRequest queued_request;
IdentityMintRequestQueue::MintType type =
IdentityMintRequestQueue::MINT_TYPE_INTERACTIVE;
EXPECT_CALL(queued_request, StartMintToken(type)).Times(1);
QueueRequestStart(type, &queued_request);
func->push_mint_token_result(TestOAuth2MintTokenFlow::MINT_TOKEN_SUCCESS);
std::string requested_account_arg =
requested_account.has_value()
? ", \"account\": {\"id\": \"" + requested_account.value() + "\"}"
: "";
RunFunctionAsync(func.get(),
"[{\"interactive\": true" + requested_account_arg + "}]");
base::RunLoop().RunUntilIdle();
EXPECT_EQ(expected_selected_user_id, func->GetSelectedUserId());
// Resume the function
QueueRequestComplete(type, &queued_request);
// Complete function and do some basic checks.
std::string access_token;
std::set<std::string> granted_scopes;
WaitForGetAuthTokenResults(func.get(), &access_token, &granted_scopes);
EXPECT_EQ(kAccessToken, access_token);
EXPECT_EQ(func->GetExtensionTokenKeyForTest()->scopes, granted_scopes);
}
};
// Tests that Chrome uses the correct selected user id value when a gaia id was
// cached and only the primary account is signed in.
IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionSelectedUserIdTest, SingleAccount) {
auto extension = base::WrapRefCounted(CreateExtension(CLIENT_ID | SCOPES));
SignIn("primary@example.com");
CoreAccountInfo primary_account = GetPrimaryAccountInfo();
SetCachedGaiaId(primary_account.gaia);
RunNewFunctionAndExpectSelectedUserId(extension, primary_account.gaia);
histogram_tester()->ExpectUniqueSample(
kGetAuthTokenResultHistogramName, IdentityGetAuthTokenError::State::kNone,
1);
}
// Tests that Chrome uses the correct selected user id value when a gaia id was
// cached for a secondary account.
IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionSelectedUserIdTest,
MultipleAccounts) {
// This test requires the use of a secondary account. If extensions are
// restricted to primary account only, this test wouldn't make too much sense.
if (id_api()->AreExtensionsRestrictedToPrimaryAccount())
return;
auto extension = base::WrapRefCounted(CreateExtension(CLIENT_ID | SCOPES));
SignIn("primary@example.com");
AccountInfo secondary_account =
identity_test_env()->MakeAccountAvailable("secondary@example.com");
SetCachedGaiaId(secondary_account.gaia);
RunNewFunctionAndExpectSelectedUserId(extension, secondary_account.gaia);
histogram_tester()->ExpectUniqueSample(
kGetAuthTokenResultHistogramName, IdentityGetAuthTokenError::State::kNone,
1);
}
// Tests that Chrome uses the correct selected user id value when a gaia id was
// cached but the extension specifies an account id for a different available
// account.
IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionSelectedUserIdTest,
RequestedAccountAvailable) {
// This test requires the use of a secondary account. If extensions are
// restricted to primary account only, this test wouldn't make too much sense.
if (id_api()->AreExtensionsRestrictedToPrimaryAccount())
return;
auto extension = base::WrapRefCounted(CreateExtension(CLIENT_ID | SCOPES));
SignIn("primary@example.com");
CoreAccountInfo primary_account = GetPrimaryAccountInfo();
AccountInfo secondary_account =
identity_test_env()->MakeAccountAvailable("secondary@example.com");
SetCachedGaiaId(primary_account.gaia);
// Run a new function with an account id specified in the arguments.
RunNewFunctionAndExpectSelectedUserId(extension, secondary_account.gaia,
secondary_account.gaia);
histogram_tester()->ExpectUniqueSample(
kGetAuthTokenResultHistogramName, IdentityGetAuthTokenError::State::kNone,
1);
}
// The signin flow is not used on ChromeOS.
#if !defined(OS_CHROMEOS)
// Tests that Chrome does not have any selected user id value if the account
// specified by the extension is not available.
IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionSelectedUserIdTest,
RequestedAccountUnavailable) {
// This test requires the use of a secondary account. If extensions are
// restricted to primary account only, this test wouldn't make too much sense.
if (id_api()->AreExtensionsRestrictedToPrimaryAccount())
return;
auto extension = base::WrapRefCounted(CreateExtension(CLIENT_ID | SCOPES));
SignIn("primary@example.com");
// Run a new function with an account id specified. Since this account is not
// signed in, the login screen will be shown.
auto func = base::MakeRefCounted<FakeGetAuthTokenFunction>();
func->set_extension(extension);
func->set_login_ui_result(true);
RunFunctionAndExpectSelectedUserId(func, "",
"gaia_id_for_unavailable_example.com");
// The login ui still showed but another account was logged in instead.
EXPECT_TRUE(func->login_ui_shown());
histogram_tester()->ExpectUniqueSample(
kGetAuthTokenResultHistogramName, IdentityGetAuthTokenError::State::kNone,
1);
}
// Tests that Chrome uses the correct selected user id value after logging into
// the account requested by the extension.
IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionSelectedUserIdTest,
RequestedAccountLogin) {
// This test requires the use of a secondary account. If extensions are
// restricted to primary account only, this test wouldn't make too much sense.
if (id_api()->AreExtensionsRestrictedToPrimaryAccount())
return;
auto extension = base::WrapRefCounted(CreateExtension(CLIENT_ID | SCOPES));
SignIn("primary@example.com");
// Run a new function with an account id specified. Since this account is not
// signed in, the login screen will be shown.
auto func = base::MakeRefCounted<FakeGetAuthTokenFunction>();
func->set_extension(extension);
func->set_login_ui_result(true);
RunFunctionAndExpectSelectedUserId(func, "gaia_id_for_secondary_example.com",
"gaia_id_for_secondary_example.com");
EXPECT_TRUE(func->login_ui_shown());
histogram_tester()->ExpectUniqueSample(
kGetAuthTokenResultHistogramName, IdentityGetAuthTokenError::State::kNone,
1);
}
#endif
class GetAuthTokenFunctionSelectedUserIdDisabledTest
: public GetAuthTokenFunctionSelectedUserIdTest {
public:
GetAuthTokenFunctionSelectedUserIdDisabledTest()
: GetAuthTokenFunctionSelectedUserIdTest(false) {}
};
// Tests that Chrome does not use any selected user id value if the
// 'SelectedUserIdInGetAuthToken' flag is disabled.
IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionSelectedUserIdDisabledTest,
SingleAccount) {
auto extension = base::WrapRefCounted(CreateExtension(CLIENT_ID | SCOPES));
SignIn("primary@example.com");
CoreAccountInfo primary_account = GetPrimaryAccountInfo();
SetCachedGaiaId(primary_account.gaia);
RunNewFunctionAndExpectSelectedUserId(extension, "");
histogram_tester()->ExpectUniqueSample(
kGetAuthTokenResultHistogramName, IdentityGetAuthTokenError::State::kNone,
1);
}
IN_PROC_BROWSER_TEST_F(RemoveCachedAuthTokenFunctionTest, NotFound) {
EXPECT_TRUE(InvalidateDefaultToken());
EXPECT_EQ(IdentityTokenCacheValue::CACHE_STATUS_NOTFOUND,
......
......@@ -96,6 +96,11 @@ bool IsReturnScopesInGetAuthTokenEnabled() {
extensions_features::kReturnScopesInGetAuthToken);
}
bool IsSelectedUserIdInGetAuthTokenEnabled() {
return base::FeatureList::IsEnabled(
extensions_features::kSelectedUserIdInGetAuthToken);
}
} // namespace
IdentityGetAuthTokenFunction::IdentityGetAuthTokenFunction()
......@@ -179,6 +184,7 @@ ExtensionFunction::ResponseAction IdentityGetAuthTokenFunction::Run() {
.value_or("");
}
selected_gaia_id_ = gaia_id;
// From here on out, results must be returned asynchronously.
StartAsyncRun();
......@@ -1037,13 +1043,14 @@ IdentityGetAuthTokenFunction::CreateMintTokenFlow() {
std::string signin_scoped_device_id =
GetSigninScopedDeviceIdForProfile(GetProfile());
auto mint_token_flow = std::make_unique<OAuth2MintTokenFlow>(
this, OAuth2MintTokenFlow::Parameters(
extension()->id(), oauth2_client_id_,
std::vector<std::string>(token_key_.scopes.begin(),
token_key_.scopes.end()),
enable_granular_permissions_, signin_scoped_device_id,
consent_result_, GetOAuth2MintTokenFlowVersion(),
GetOAuth2MintTokenFlowChannel(), gaia_mint_token_mode_));
this,
OAuth2MintTokenFlow::Parameters(
extension()->id(), oauth2_client_id_,
std::vector<std::string>(token_key_.scopes.begin(),
token_key_.scopes.end()),
enable_granular_permissions_, signin_scoped_device_id,
GetSelectedUserId(), consent_result_, GetOAuth2MintTokenFlowVersion(),
GetOAuth2MintTokenFlowChannel(), gaia_mint_token_mode_));
return mint_token_flow;
}
......@@ -1080,4 +1087,12 @@ bool IdentityGetAuthTokenFunction::enable_granular_permissions() const {
return enable_granular_permissions_;
}
std::string IdentityGetAuthTokenFunction::GetSelectedUserId() const {
if (IsSelectedUserIdInGetAuthTokenEnabled() &&
selected_gaia_id_ == token_key_.account_info.gaia)
return selected_gaia_id_;
return "";
}
} // namespace extensions
......@@ -118,6 +118,12 @@ class IdentityGetAuthTokenFunction : public ExtensionFunction,
Profile* GetProfile() const;
// Returns the gaia id of the account requested by or previously selected for
// this extension if the account is available on the device. Otherwise,
// returns an empty string.
// Exposed for testing.
std::string GetSelectedUserId() const;
// Pending request for an access token from the device account (via
// DeviceOAuth2TokenService).
std::unique_ptr<OAuth2AccessTokenManager::Request>
......@@ -228,6 +234,10 @@ class IdentityGetAuthTokenFunction : public ExtensionFunction,
bool should_prompt_for_signin_ = false;
bool enable_granular_permissions_ = false;
// The gaia id of the account requested by or previously selected for this
// extension.
std::string selected_gaia_id_;
// Shown in the extension login prompt.
std::string email_for_default_web_account_;
......
......@@ -57,4 +57,10 @@ const base::Feature kReportKeepaliveUkm{"ReportKeepaliveUkm",
const base::Feature kReturnScopesInGetAuthToken{
"ReturnScopesInGetAuthToken", base::FEATURE_DISABLED_BY_DEFAULT};
// If enabled, allows the GetAuthToken API to provide the "selected_user_id"
// parameter to the server, indicating which account to request permissions
// from.
const base::Feature kSelectedUserIdInGetAuthToken{
"SelectedUserIdInGetAuthToken", base::FEATURE_DISABLED_BY_DEFAULT};
} // namespace extensions_features
......@@ -31,6 +31,8 @@ extern const base::Feature kReportKeepaliveUkm;
extern const base::Feature kReturnScopesInGetAuthToken;
extern const base::Feature kSelectedUserIdInGetAuthToken;
} // namespace extensions_features
#endif // EXTENSIONS_COMMON_EXTENSION_FEATURES_H_
......@@ -45,6 +45,8 @@ const char kOAuth2IssueTokenBodyFormat[] =
"&origin=%s"
"&lib_ver=%s"
"&release_channel=%s";
const char kOAuth2IssueTokenBodyFormatSelectedUserIdAddendum[] =
"&selected_user_id=%s";
const char kOAuth2IssueTokenBodyFormatDeviceIdAddendum[] =
"&device_id=%s&device_type=chrome";
const char kOAuth2IssueTokenBodyFormatConsentResultAddendum[] =
......@@ -150,6 +152,7 @@ OAuth2MintTokenFlow::Parameters::Parameters(
const std::vector<std::string>& scopes_arg,
bool enable_granular_permissions,
const std::string& device_id,
const std::string& selected_user_id,
const std::string& consent_result,
const std::string& version,
const std::string& channel,
......@@ -159,6 +162,7 @@ OAuth2MintTokenFlow::Parameters::Parameters(
scopes(scopes_arg),
enable_granular_permissions(enable_granular_permissions),
device_id(device_id),
selected_user_id(selected_user_id),
consent_result(consent_result),
version(version),
channel(channel),
......@@ -240,6 +244,11 @@ std::string OAuth2MintTokenFlow::CreateApiCallBody() {
kOAuth2IssueTokenBodyFormatDeviceIdAddendum,
net::EscapeUrlEncodedData(parameters_.device_id, true).c_str()));
}
if (!parameters_.selected_user_id.empty()) {
body.append(base::StringPrintf(
kOAuth2IssueTokenBodyFormatSelectedUserIdAddendum,
net::EscapeUrlEncodedData(parameters_.selected_user_id, true).c_str()));
}
if (!parameters_.consent_result.empty()) {
body.append(base::StringPrintf(
kOAuth2IssueTokenBodyFormatConsentResultAddendum,
......
......@@ -117,6 +117,7 @@ class OAuth2MintTokenFlow : public OAuth2ApiCallFlow {
const std::vector<std::string>& scopes_arg,
bool enable_granular_permissions,
const std::string& device_id,
const std::string& selected_user_id,
const std::string& consent_result,
const std::string& version,
const std::string& channel,
......@@ -129,6 +130,7 @@ class OAuth2MintTokenFlow : public OAuth2ApiCallFlow {
std::vector<std::string> scopes;
bool enable_granular_permissions;
std::string device_id;
std::string selected_user_id;
std::string consent_result;
std::string version;
std::string channel;
......
......@@ -235,29 +235,35 @@ class OAuth2MintTokenFlowTest : public testing::Test {
const network::mojom::URLResponseHeadPtr head_200_;
void CreateFlow(OAuth2MintTokenFlow::Mode mode) {
return CreateFlow(&delegate_, mode, false, "", "");
return CreateFlow(&delegate_, mode, false, "", "", "");
}
void CreateFlowWithEnableGranularPermissions(
const bool enable_granular_permissions) {
return CreateFlow(&delegate_, OAuth2MintTokenFlow::MODE_ISSUE_ADVICE,
enable_granular_permissions, "", "");
enable_granular_permissions, "", "", "");
}
void CreateFlowWithDeviceId(const std::string& device_id) {
return CreateFlow(&delegate_, OAuth2MintTokenFlow::MODE_ISSUE_ADVICE, false,
device_id, "");
device_id, "", "");
}
void CreateFlowWithSelectedUserId(const std::string& selected_user_id) {
return CreateFlow(&delegate_, OAuth2MintTokenFlow::MODE_ISSUE_ADVICE, false,
"", selected_user_id, "");
}
void CreateFlowWithConsentResult(const std::string& consent_result) {
return CreateFlow(&delegate_, OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE,
false, "", consent_result);
false, "", "", consent_result);
}
void CreateFlow(MockDelegate* delegate,
OAuth2MintTokenFlow::Mode mode,
const bool enable_granular_permissions,
const std::string& device_id,
const std::string& selected_user_id,
const std::string& consent_result) {
std::string ext_id = "ext1";
std::string client_id = "client1";
......@@ -265,9 +271,10 @@ class OAuth2MintTokenFlowTest : public testing::Test {
std::string channel = "test_channel";
std::vector<std::string> scopes(CreateTestScopes());
flow_ = std::make_unique<MockMintTokenFlow>(
delegate, OAuth2MintTokenFlow::Parameters(
ext_id, client_id, scopes, enable_granular_permissions,
device_id, consent_result, version, channel, mode));
delegate,
OAuth2MintTokenFlow::Parameters(
ext_id, client_id, scopes, enable_granular_permissions, device_id,
selected_user_id, consent_result, version, channel, mode));
}
void ProcessApiCallSuccess(const network::mojom::URLResponseHead* head,
......@@ -381,6 +388,21 @@ TEST_F(OAuth2MintTokenFlowTest, CreateApiCallBody) {
"&device_type=chrome");
EXPECT_EQ(expected_body, body);
}
{
CreateFlowWithSelectedUserId("user_id1");
std::string body = flow_->CreateApiCallBody();
std::string expected_body(
"force=false"
"&response_type=none"
"&scope=http://scope1+http://scope2"
"&enable_granular_permissions=false"
"&client_id=client1"
"&origin=ext1"
"&lib_ver=test_version"
"&release_channel=test_channel"
"&selected_user_id=user_id1");
EXPECT_EQ(expected_body, body);
}
{
CreateFlowWithConsentResult("consent1");
std::string body = flow_->CreateApiCallBody();
......@@ -707,7 +729,7 @@ TEST_F(OAuth2MintTokenFlowTest, ProcessApiCallSuccess_RemoteConsentFailure) {
TEST_F(OAuth2MintTokenFlowTest, ProcessApiCallFailure_NullDelegate) {
network::mojom::URLResponseHead head;
CreateFlow(nullptr, OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE, false, "",
"");
"", "");
ProcessApiCallFailure(net::ERR_FAILED, &head, nullptr);
histogram_tester_.ExpectUniqueSample(
kOAuth2MintTokenApiCallResultHistogram,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment