blob: 748ba4acabc2a94717a668f4989028650bf7a670 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/webid/federated_auth_request_impl.h"
#include <memory>
#include <string>
#include <utility>
#include "base/optional.h"
#include "base/run_loop.h"
#include "base/test/task_environment.h"
#include "content/browser/webid/id_token_request_callback_data.h"
#include "content/browser/webid/test/mock_identity_request_dialog_controller.h"
#include "content/browser/webid/test/mock_idp_network_request_manager.h"
#include "content/public/test/test_renderer_host.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/webid/federated_auth_request.mojom.h"
#include "url/gurl.h"
using blink::mojom::RequestIdTokenStatus;
using ::testing::_;
using ::testing::Invoke;
using ::testing::NiceMock;
namespace content {
namespace {
constexpr char kIdpTestOrigin[] = "https://idp.example";
constexpr char kIdpEndpoint[] = "https://idp.example/webid";
constexpr char kSigninUrl[] = "https://idp.example/signin";
// Values will be added here as token introspection is implemented.
constexpr char kAuthRequest[] = "";
constexpr char kToken[] = "[not a real token]";
constexpr char kEmptyToken[] = "";
// Parameters for a call to RequestIdToken.
typedef struct {
const char* provider;
const char* request;
} RequestParameters;
// Expected return values from a call to RequestIdToken.
typedef struct {
RequestIdTokenStatus return_status;
const char* token;
} RequestExpectations;
// Mock configuration values for test.
typedef struct {
const char* token;
IdentityRequestDialogController::UserApproval initial_permission;
base::Optional<IdpNetworkRequestManager::FetchStatus> wellknown_fetch_status;
const char* idp_endpoint;
base::Optional<IdpNetworkRequestManager::SigninResponse> signin_response;
const char* signin_url_or_token;
base::Optional<IdentityRequestDialogController::UserApproval>
token_permission;
} MockConfiguration;
// base::Optional fields should be nullopt to prevent the corresponding
// methods from having EXPECT_CALL set on the mocks.
typedef struct {
RequestParameters inputs;
RequestExpectations expected;
MockConfiguration config;
} AuthRequestTestCase;
constexpr AuthRequestTestCase kAllTestCases[]{
// Successful run with the IdP page loaded.
{{kIdpTestOrigin, kAuthRequest},
{RequestIdTokenStatus::kSuccess, kToken},
{kToken, IdentityRequestDialogController::UserApproval::kApproved,
IdpNetworkRequestManager::FetchStatus::kSuccess, kIdpEndpoint,
IdpNetworkRequestManager::SigninResponse::kLoadIdp, kSigninUrl,
IdentityRequestDialogController::UserApproval::kApproved}},
// Successful run with a token response from the idp_endpoint.
{{kIdpTestOrigin, kAuthRequest},
{RequestIdTokenStatus::kSuccess, kToken},
{kToken, IdentityRequestDialogController::UserApproval::kApproved,
IdpNetworkRequestManager::FetchStatus::kSuccess, kIdpEndpoint,
IdpNetworkRequestManager::SigninResponse::kTokenGranted, kToken,
base::nullopt}},
// Initial user permission denied.
{{kIdpTestOrigin, kAuthRequest},
{RequestIdTokenStatus::kApprovalDeclined, kEmptyToken},
{kToken, IdentityRequestDialogController::UserApproval::kDenied,
base::nullopt, "", base::nullopt, "", base::nullopt}},
// Well-known file not found.
{{kIdpTestOrigin, kAuthRequest},
{RequestIdTokenStatus::kErrorWebIdNotSupportedByProvider, kEmptyToken},
{kToken, IdentityRequestDialogController::UserApproval::kApproved,
IdpNetworkRequestManager::FetchStatus::kWebIdNotSupported, "",
base::nullopt, "", base::nullopt}},
// Well-known fetch error.
{{kIdpTestOrigin, kAuthRequest},
{RequestIdTokenStatus::kErrorFetchingWellKnown, kEmptyToken},
{kToken, IdentityRequestDialogController::UserApproval::kApproved,
IdpNetworkRequestManager::FetchStatus::kFetchError, "", base::nullopt, "",
base::nullopt}},
// Error parsing well-known.
{{kIdpTestOrigin, kAuthRequest},
{RequestIdTokenStatus::kErrorInvalidWellKnown, kEmptyToken},
{kToken, IdentityRequestDialogController::UserApproval::kApproved,
IdpNetworkRequestManager::FetchStatus::kInvalidResponseError, "",
base::nullopt, "", base::nullopt}},
// Error reaching the idp_endpoint.
{{kIdpTestOrigin, kAuthRequest},
{RequestIdTokenStatus::kErrorFetchingSignin, kEmptyToken},
{kToken, IdentityRequestDialogController::UserApproval::kApproved,
IdpNetworkRequestManager::FetchStatus::kSuccess, kIdpEndpoint,
IdpNetworkRequestManager::SigninResponse::kSigninError, "",
base::nullopt}},
// Error parsing the idp_endpoint response.
{{kIdpTestOrigin, kAuthRequest},
{RequestIdTokenStatus::kErrorInvalidSigninResponse, kEmptyToken},
{kToken, IdentityRequestDialogController::UserApproval::kApproved,
IdpNetworkRequestManager::FetchStatus::kSuccess, kIdpEndpoint,
IdpNetworkRequestManager::SigninResponse::kInvalidResponseError, "",
base::nullopt}},
// IdP window closed before token provision.
{{kIdpTestOrigin, kAuthRequest},
{RequestIdTokenStatus::kError, kEmptyToken},
{kEmptyToken, IdentityRequestDialogController::UserApproval::kApproved,
IdpNetworkRequestManager::FetchStatus::kSuccess, kIdpEndpoint,
IdpNetworkRequestManager::SigninResponse::kLoadIdp, kSigninUrl,
base::nullopt}},
// Token provision declined by user after IdP window closed.
{{kIdpTestOrigin, kAuthRequest},
{RequestIdTokenStatus::kApprovalDeclined, kEmptyToken},
{kToken, IdentityRequestDialogController::UserApproval::kApproved,
IdpNetworkRequestManager::FetchStatus::kSuccess, kIdpEndpoint,
IdpNetworkRequestManager::SigninResponse::kLoadIdp, kSigninUrl,
IdentityRequestDialogController::UserApproval::kDenied}},
};
// Helper class for receiving the mojo method callback.
class AuthRequestCallbackHelper {
public:
AuthRequestCallbackHelper() = default;
~AuthRequestCallbackHelper() = default;
AuthRequestCallbackHelper(const AuthRequestCallbackHelper&) = delete;
AuthRequestCallbackHelper& operator=(const AuthRequestCallbackHelper&) =
delete;
RequestIdTokenStatus status() const { return status_; }
base::Optional<std::string> token() const { return token_; }
// This can only be called once per lifetime of this object.
base::OnceCallback<void(RequestIdTokenStatus,
const base::Optional<std::string>&)>
callback() {
return base::BindOnce(&AuthRequestCallbackHelper::ReceiverMethod,
base::Unretained(this));
}
// Returns when callback() is called, which can be immediately if it has
// already been called.
void WaitForCallback() {
if (was_called_)
return;
wait_for_callback_loop_.Run();
}
private:
void ReceiverMethod(RequestIdTokenStatus status,
const base::Optional<std::string>& token) {
status_ = status;
token_ = token;
was_called_ = true;
wait_for_callback_loop_.Quit();
}
bool was_called_ = false;
base::RunLoop wait_for_callback_loop_;
RequestIdTokenStatus status_;
base::Optional<std::string> token_;
};
} // namespace
class FederatedAuthRequestImplTest : public RenderViewHostTestHarness {
protected:
FederatedAuthRequestImplTest()
: RenderViewHostTestHarness(
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
~FederatedAuthRequestImplTest() override = default;
void CreateAuthRequest(const GURL& provider) {
provider_ = provider;
auth_request_impl_ = std::make_unique<FederatedAuthRequestImpl>(
main_rfh(), request_remote_.BindNewPipeAndPassReceiver());
mock_request_manager_ =
std::make_unique<NiceMock<MockIdpNetworkRequestManager>>(provider,
main_rfh());
mock_dialog_controller_ =
std::make_unique<NiceMock<MockIdentityRequestDialogController>>();
}
std::pair<RequestIdTokenStatus, base::Optional<std::string>>
PerformAuthRequest(const std::string& request) {
auth_request_impl_->SetNetworkManagerForTests(
std::move(mock_request_manager_));
auth_request_impl_->SetDialogControllerForTests(
std::move(mock_dialog_controller_));
AuthRequestCallbackHelper auth_helper;
request_remote_->RequestIdToken(provider_, request, auth_helper.callback());
auth_helper.WaitForCallback();
return std::make_pair(auth_helper.status(), auth_helper.token());
}
void SetMockExpectations(const AuthRequestTestCase& test_case) {
EXPECT_CALL(*mock_dialog_controller_, ShowInitialPermissionDialog(_, _, _))
.WillOnce(
Invoke([&](WebContents*, const GURL&,
IdentityRequestDialogController::InitialApprovalCallback
callback) {
std::move(callback).Run(test_case.config.initial_permission);
}));
if (test_case.config.wellknown_fetch_status) {
EXPECT_CALL(*mock_request_manager_, FetchIdpWellKnown(_))
.WillOnce(Invoke(
[&](IdpNetworkRequestManager::FetchWellKnownCallback callback) {
std::move(callback).Run(
*test_case.config.wellknown_fetch_status,
test_case.config.idp_endpoint);
}));
}
if (test_case.config.signin_response) {
EXPECT_CALL(*mock_request_manager_, SendSigninRequest(_, _, _))
.WillOnce(Invoke(
[&](const GURL&, const std::string&,
IdpNetworkRequestManager::SigninRequestCallback callback) {
std::move(callback).Run(*test_case.config.signin_response,
test_case.config.signin_url_or_token);
}));
}
// The IdP dialog only shows when kLoadIdP is the return code from the
// signin request.
if (test_case.config.signin_response ==
IdpNetworkRequestManager::SigninResponse::kLoadIdp) {
EXPECT_CALL(*mock_dialog_controller_, ShowIdProviderWindow(_, _, _, _))
.WillOnce(Invoke([&](WebContents*, WebContents* idp_web_contents,
const GURL&,
IdentityRequestDialogController::
IdProviderWindowClosedCallback callback) {
close_idp_window_callback_ = std::move(callback);
auto* request_callback_data =
IdTokenRequestCallbackData::Get(idp_web_contents);
EXPECT_TRUE(request_callback_data);
auto rp_done_callback = request_callback_data->TakeDoneCallback();
IdTokenRequestCallbackData::Remove(idp_web_contents);
EXPECT_TRUE(rp_done_callback);
std::move(rp_done_callback).Run(test_case.config.token);
}));
EXPECT_CALL(*mock_dialog_controller_, CloseIdProviderWindow())
.WillOnce(
Invoke([&]() { std::move(close_idp_window_callback_).Run(); }));
}
if (test_case.config.token_permission) {
EXPECT_CALL(*mock_dialog_controller_,
ShowTokenExchangePermissionDialog(_, _, _))
.WillOnce(Invoke(
[&](content::WebContents* idp_web_contents, const GURL& idp_url,
IdentityRequestDialogController::TokenExchangeApprovalCallback
callback) {
std::move(callback).Run(*test_case.config.token_permission);
}));
}
}
private:
mojo::Remote<blink::mojom::FederatedAuthRequest> request_remote_;
std::unique_ptr<FederatedAuthRequestImpl> auth_request_impl_;
std::unique_ptr<NiceMock<MockIdpNetworkRequestManager>> mock_request_manager_;
std::unique_ptr<NiceMock<MockIdentityRequestDialogController>>
mock_dialog_controller_;
base::OnceClosure close_idp_window_callback_;
GURL provider_;
};
class BasicFederatedAuthRequestImplTest
: public FederatedAuthRequestImplTest,
public ::testing::WithParamInterface<AuthRequestTestCase> {};
INSTANTIATE_TEST_SUITE_P(FederatedAuthRequestImplTests,
BasicFederatedAuthRequestImplTest,
::testing::ValuesIn(kAllTestCases));
// Exercise all configured test cases in kAllTestCases.
TEST_P(BasicFederatedAuthRequestImplTest, FederatedAuthRequests) {
AuthRequestTestCase test_case = GetParam();
CreateAuthRequest(GURL(test_case.inputs.provider));
SetMockExpectations(test_case);
auto auth_response = PerformAuthRequest(test_case.inputs.request);
EXPECT_EQ(auth_response.first, test_case.expected.return_status);
EXPECT_EQ(auth_response.second, test_case.expected.token);
}
} // namespace content