// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/webauth/webauth_request_security_checker_impl.h"

#include <string_view>

#include "base/json/json_reader.h"
#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/values.h"
#include "components/webauthn/core/browser/remote_validation.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/browser/web_authentication_delegate.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_web_contents_factory.h"
#include "device/fido/public/features.h"
#include "services/network/public/cpp/content_security_policy/content_security_policy.h"
#include "services/network/public/cpp/permissions_policy/permissions_policy_declaration.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-shared.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom.h"
#include "third_party/blink/public/mojom/webauthn/authenticator.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_util.h"

namespace content {
namespace {

using ::testing::_;

// A minimal WebAuthenticationDelegate that authorizes a single caller origin to
// use the remote desktop client override, so tests can exercise the delegated
// (remoteClientDataJSON) RP ID validation path.
class FakeWebAuthenticationDelegate : public WebAuthenticationDelegate {
 public:
  bool OriginMayUseRemoteDesktopClientOverride(
      BrowserContext* browser_context,
      const url::Origin& caller_origin) override {
    return authorized_origin.has_value() && caller_origin == *authorized_origin;
  }

  std::optional<url::Origin> authorized_origin;
};

class MockWebAuthnContentBrowserClient : public ContentBrowserClient {
 public:
  MockWebAuthnContentBrowserClient() = default;
  ~MockWebAuthnContentBrowserClient() override = default;

  MOCK_METHOD(void,
              LogWebFeatureForCurrentPage,
              (RenderFrameHost*, blink::mojom::WebFeature),
              (override));

  scoped_refptr<network::SharedURLLoaderFactory>
  GetSystemSharedURLLoaderFactory() override {
    return shared_url_loader_factory_;
  }

  void set_shared_url_loader_factory(
      scoped_refptr<network::SharedURLLoaderFactory> factory) {
    shared_url_loader_factory_ = std::move(factory);
  }

  WebAuthenticationDelegate* GetWebAuthenticationDelegate() override {
    return &web_authentication_delegate;
  }

  FakeWebAuthenticationDelegate web_authentication_delegate;

 private:
  scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory_;
};

network::ParsedPermissionsPolicy CreatePolicyToAllowWebAuthn() {
  return {network::ParsedPermissionsPolicyDeclaration(
      network::mojom::PermissionsPolicyFeature::kPublicKeyCredentialsGet,
      /*allowed_origins=*/{}, /*self_if_matches=*/std::nullopt,
      /*matches_all_origins=*/true,
      /*matches_opaque_src=*/false)};
}

// The default policy allows same-origin with ancestors, but this creates one
// with value 'none'.
network::ParsedPermissionsPolicy CreatePolicyToDenyWebAuthn() {
  return {network::ParsedPermissionsPolicyDeclaration(
      network::mojom::PermissionsPolicyFeature::kPublicKeyCredentialsGet,
      /*allowed_origins=*/{}, /*self_if_matches=*/std::nullopt,
      /*matches_all_origins=*/false,
      /*matches_opaque_src=*/false)};
}

network::ParsedPermissionsPolicy CreatePolicyToAllowWebPayments() {
  return {network::ParsedPermissionsPolicyDeclaration(
      network::mojom::PermissionsPolicyFeature::kPayment,
      /*allowed_origins=*/{},
      /*self_if_matches=*/std::nullopt,
      /*matches_all_origins=*/true, /*matches_opaque_src=*/false)};
}

struct TestCase {
  TestCase(const std::string_view& url,
           const network::ParsedPermissionsPolicy& policy,
           WebAuthRequestSecurityChecker::RequestType request_type,
           bool expected_is_cross_origin,
           blink::mojom::AuthenticatorStatus expected_status)
      : url(url),
        policy(policy),
        request_type(request_type),
        expected_is_cross_origin(expected_is_cross_origin),
        expected_status(expected_status) {}

  ~TestCase() = default;

  const std::string_view url;
  const network::ParsedPermissionsPolicy policy;
  const WebAuthRequestSecurityChecker::RequestType request_type;
  const bool expected_is_cross_origin;
  const blink::mojom::AuthenticatorStatus expected_status;
};

std::ostream& operator<<(std::ostream& out, const TestCase& test_case) {
  out << test_case.url << " ";
  switch (test_case.request_type) {
    case WebAuthRequestSecurityChecker::RequestType::kGetAssertion:
      out << "Get Assertion";
      break;
    case WebAuthRequestSecurityChecker::RequestType::
        kGetPaymentCredentialAssertion:
      out << "Get Payment Credential Assertion";
      break;
    case WebAuthRequestSecurityChecker::RequestType::kMakePaymentCredential:
      out << "Make Payment Credential";
      break;
    case WebAuthRequestSecurityChecker::RequestType::kMakeCredential:
      out << "Make Credential";
      break;
    case WebAuthRequestSecurityChecker::RequestType::kReport:
      out << "Report";
      break;
  }
  return out;
}

class WebAuthRequestSecurityCheckerTest
    : public testing::TestWithParam<TestCase> {
 protected:
  WebAuthRequestSecurityCheckerTest()
      : web_contents_(web_contents_factory_.CreateWebContents(&context_)) {}

  ~WebAuthRequestSecurityCheckerTest() override = default;

  content::WebContents* web_contents() const { return web_contents_; }

 private:
  // Must be first because ScopedFeatureList must be initialized before other
  // threads are started.
  base::test::ScopedFeatureList features_{
      /*enable_feature=*/features::kSecurePaymentConfirmation};
  content::BrowserTaskEnvironment task_environment_;
  content::TestBrowserContext context_;
  content::TestWebContentsFactory web_contents_factory_;
  raw_ptr<content::WebContents>
      web_contents_;  // Owned by `web_contents_factory_`.
};

TEST_P(WebAuthRequestSecurityCheckerTest, ValidateAncestorOrigins) {
  RenderFrameHost* main_frame =
      NavigationSimulator::NavigateAndCommitFromBrowser(
          web_contents(), GURL("https://same-origin.com"));
  ASSERT_NE(nullptr, main_frame);
  RenderFrameHostTester* tester = RenderFrameHostTester::For(main_frame);
  RenderFrameHost* sub_frame =
      tester->AppendChildWithPolicy("sub_frame", GetParam().policy);
  ASSERT_NE(nullptr, sub_frame);
  sub_frame = NavigationSimulator::NavigateAndCommitFromDocument(
      GURL(GetParam().url), sub_frame);
  auto checker = static_cast<RenderFrameHostImpl*>(sub_frame)
                     ->GetWebAuthRequestSecurityCheckerImpl();

  bool actual_is_cross_origin = false;
  blink::mojom::AuthenticatorStatus actual_status =
      checker->ValidateAncestorOrigins(
          url::Origin::Create(GURL(GetParam().url)), GetParam().request_type,
          &actual_is_cross_origin);

  EXPECT_EQ(GetParam().expected_status, actual_status);
  EXPECT_EQ(GetParam().expected_is_cross_origin, actual_is_cross_origin);
}

INSTANTIATE_TEST_SUITE_P(
    ProhibitCrossOrigin,
    WebAuthRequestSecurityCheckerTest,
    testing::Values(
        TestCase("https://same-origin.com",
                 network::ParsedPermissionsPolicy(),
                 WebAuthRequestSecurityChecker::RequestType::kGetAssertion,
                 /*expected_is_cross_origin=*/false,
                 blink::mojom::AuthenticatorStatus::SUCCESS),
        TestCase("https://cross-origin.com",
                 network::ParsedPermissionsPolicy(),
                 WebAuthRequestSecurityChecker::RequestType::kGetAssertion,
                 /*expected_is_cross_origin=*/true,
                 blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR),
        TestCase("https://same-origin.com",
                 network::ParsedPermissionsPolicy(),
                 WebAuthRequestSecurityChecker::RequestType::kMakeCredential,
                 /*expected_is_cross_origin=*/false,
                 blink::mojom::AuthenticatorStatus::SUCCESS),
        TestCase("https://cross-origin.com",
                 network::ParsedPermissionsPolicy(),
                 WebAuthRequestSecurityChecker::RequestType::kMakeCredential,
                 /*expected_is_cross_origin=*/true,
                 blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR),
        TestCase(
            "https://same-origin.com",
            network::ParsedPermissionsPolicy(),
            WebAuthRequestSecurityChecker::RequestType::kMakePaymentCredential,
            /*expected_is_cross_origin=*/false,
            blink::mojom::AuthenticatorStatus::SUCCESS),
        TestCase(
            "https://cross-origin.com",
            network::ParsedPermissionsPolicy(),
            WebAuthRequestSecurityChecker::RequestType::kMakePaymentCredential,
            /*expected_is_cross_origin=*/true,
            blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR)));

INSTANTIATE_TEST_SUITE_P(
    AllowCrossOriginWebAuthn,
    WebAuthRequestSecurityCheckerTest,
    testing::Values(
        TestCase("https://same-origin.com",
                 CreatePolicyToAllowWebAuthn(),
                 WebAuthRequestSecurityChecker::RequestType::kGetAssertion,
                 /*expected_is_cross_origin=*/false,
                 blink::mojom::AuthenticatorStatus::SUCCESS),
        TestCase("https://cross-origin.com",
                 CreatePolicyToAllowWebAuthn(),
                 WebAuthRequestSecurityChecker::RequestType::kGetAssertion,
                 /*expected_is_cross_origin=*/true,
                 blink::mojom::AuthenticatorStatus::SUCCESS),
        TestCase("https://same-origin.com",
                 CreatePolicyToAllowWebAuthn(),
                 WebAuthRequestSecurityChecker::RequestType::kMakeCredential,
                 /*expected_is_cross_origin=*/false,
                 blink::mojom::AuthenticatorStatus::SUCCESS),
        TestCase("https://cross-origin.com",
                 CreatePolicyToAllowWebAuthn(),
                 WebAuthRequestSecurityChecker::RequestType::kMakeCredential,
                 /*expected_is_cross_origin=*/true,
                 blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR),
        TestCase(
            "https://same-origin.com",
            CreatePolicyToAllowWebAuthn(),
            WebAuthRequestSecurityChecker::RequestType::kMakePaymentCredential,
            /*expected_is_cross_origin=*/false,
            blink::mojom::AuthenticatorStatus::SUCCESS),
        TestCase(
            "https://cross-origin.com",
            CreatePolicyToAllowWebAuthn(),
            WebAuthRequestSecurityChecker::RequestType::kMakePaymentCredential,
            /*expected_is_cross_origin=*/true,
            blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR)));

INSTANTIATE_TEST_SUITE_P(
    AllowCrossOriginPay,
    WebAuthRequestSecurityCheckerTest,
    testing::Values(
        TestCase("https://same-origin.com",
                 CreatePolicyToAllowWebPayments(),
                 WebAuthRequestSecurityChecker::RequestType::kGetAssertion,
                 /*expected_is_cross_origin=*/false,
                 blink::mojom::AuthenticatorStatus::SUCCESS),
        TestCase("https://cross-origin.com",
                 CreatePolicyToAllowWebPayments(),
                 WebAuthRequestSecurityChecker::RequestType::kGetAssertion,
                 /*expected_is_cross_origin=*/true,
                 blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR),
        TestCase("https://same-origin.com",
                 CreatePolicyToAllowWebPayments(),
                 WebAuthRequestSecurityChecker::RequestType::kMakeCredential,
                 /*expected_is_cross_origin=*/false,
                 blink::mojom::AuthenticatorStatus::SUCCESS),
        TestCase("https://cross-origin.com",
                 CreatePolicyToAllowWebPayments(),
                 WebAuthRequestSecurityChecker::RequestType::kMakeCredential,
                 /*expected_is_cross_origin=*/true,
                 blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR),
        TestCase(
            "https://same-origin.com",
            CreatePolicyToAllowWebPayments(),
            WebAuthRequestSecurityChecker::RequestType::kMakePaymentCredential,
            /*expected_is_cross_origin=*/false,
            blink::mojom::AuthenticatorStatus::SUCCESS),
        TestCase(
            "https://cross-origin.com",
            CreatePolicyToAllowWebPayments(),
            WebAuthRequestSecurityChecker::RequestType::kMakePaymentCredential,
            /*expected_is_cross_origin=*/true,
            blink::mojom::AuthenticatorStatus::SUCCESS)));

struct SingleFrameTestCase {
  SingleFrameTestCase(const network::ParsedPermissionsPolicy& policy,
                      WebAuthRequestSecurityChecker::RequestType request_type,
                      blink::mojom::AuthenticatorStatus expected_status)
      : policy(policy),
        request_type(request_type),
        expected_status(expected_status) {}

  ~SingleFrameTestCase() = default;

  const network::ParsedPermissionsPolicy policy;
  const WebAuthRequestSecurityChecker::RequestType request_type;
  const blink::mojom::AuthenticatorStatus expected_status;
};

class WebAuthRequestSecurityCheckerSingleFrameTest
    : public testing::TestWithParam<SingleFrameTestCase> {
 protected:
  WebAuthRequestSecurityCheckerSingleFrameTest()
      : web_contents_(web_contents_factory_.CreateWebContents(&context_)) {}

  ~WebAuthRequestSecurityCheckerSingleFrameTest() override = default;

  content::WebContents* web_contents() const { return web_contents_; }

 private:
  // Must be first because ScopedFeatureList must be initialized before other
  // threads are started.
  base::test::ScopedFeatureList features_{
      /*enable_feature=*/features::kSecurePaymentConfirmation};
  content::BrowserTaskEnvironment task_environment_;
  content::TestBrowserContext context_;
  content::TestWebContentsFactory web_contents_factory_;
  raw_ptr<content::WebContents>
      web_contents_;  // Owned by `web_contents_factory_`.
};

TEST_P(WebAuthRequestSecurityCheckerSingleFrameTest,
       ValidateAncestorOriginsOnRoot) {
  auto navigation = NavigationSimulator::CreateBrowserInitiated(
      GURL("https://same-origin.com"), web_contents());
  navigation->SetPermissionsPolicyHeader(GetParam().policy);
  navigation->Commit();
  ASSERT_NE(nullptr, web_contents()->GetPrimaryMainFrame());

  auto checker =
      static_cast<RenderFrameHostImpl*>(web_contents()->GetPrimaryMainFrame())
          ->GetWebAuthRequestSecurityCheckerImpl();

  bool actual_is_cross_origin = false;
  blink::mojom::AuthenticatorStatus actual_status =
      checker->ValidateAncestorOrigins(
          url::Origin::Create(GURL("https://same-origin.com")),
          GetParam().request_type, &actual_is_cross_origin);

  EXPECT_EQ(GetParam().expected_status, actual_status);
  EXPECT_EQ(false, actual_is_cross_origin);
}

INSTANTIATE_TEST_SUITE_P(
    WebAuthnSingleFrame,
    WebAuthRequestSecurityCheckerSingleFrameTest,
    testing::Values(
        SingleFrameTestCase(
            CreatePolicyToAllowWebAuthn(),
            WebAuthRequestSecurityChecker::RequestType::kGetAssertion,
            blink::mojom::AuthenticatorStatus::SUCCESS),
        SingleFrameTestCase(
            CreatePolicyToDenyWebAuthn(),
            WebAuthRequestSecurityChecker::RequestType::kGetAssertion,
            blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR),
        SingleFrameTestCase(
            CreatePolicyToAllowWebAuthn(),
            WebAuthRequestSecurityChecker::RequestType::kMakeCredential,
            blink::mojom::AuthenticatorStatus::SUCCESS),
        SingleFrameTestCase(
            CreatePolicyToDenyWebAuthn(),
            WebAuthRequestSecurityChecker::RequestType::kMakeCredential,
            blink::mojom::AuthenticatorStatus::SUCCESS)));

class WebAuthRequestSecurityCheckerWellKnownJSONTest : public testing::Test {
 protected:
  webauthn::ValidationStatus Test(std::string_view caller_origin_str,
                                  std::string_view json) {
    GURL caller_origin_url(caller_origin_str);
    CHECK(caller_origin_url.is_valid()) << caller_origin_str;

    return webauthn::RemoteValidation::ValidateWellKnownJSON(
        url::Origin::Create(caller_origin_url), json);
  }
};

TEST_F(WebAuthRequestSecurityCheckerWellKnownJSONTest, Inputs) {
  struct TestCase {
    const char* json;
    webauthn::ValidationStatus expected;
  };
  constexpr webauthn::ValidationStatus parse_error =
      webauthn::ValidationStatus::kJsonParseError;
  constexpr webauthn::ValidationStatus ok =
      webauthn::ValidationStatus::kSuccess;
  constexpr webauthn::ValidationStatus no_match =
      webauthn::ValidationStatus::kNoJsonMatch;
  constexpr webauthn::ValidationStatus no_match_hit_limits =
      webauthn::ValidationStatus::kNoJsonMatchHitLimits;

  static const TestCase kTestCases[] = {
      {R"([])", parse_error},
      {R"({})", parse_error},
      {R"({"foo": "bar"})", parse_error},
      {R"({"origins": "bar"})", parse_error},
      {R"({"origins": []})", no_match},
      {R"({"origins": [1]})", parse_error},
      {R"({"origins": ["https://foo.com"]})", ok},
      {R"({"origins": ["https://foo2.com"]})", no_match},
      {R"({"origins": ["https://com"]})", no_match},
      {R"({"origins": ["other://foo.com"]})", no_match},
      {R"({"origins": [
            "https://a.com",
            "https://b.com",
            "https://c.com",
            "https://d.com",
            "https://foo.com"
          ]})",
       ok},
      // Too many eTLD+1 labels.
      {R"({"origins": [
            "https://a.com",
            "https://b.com",
            "https://c.com",
            "https://d.com",
            "https://e.com",
            "https://foo.com"
          ]})",
       no_match_hit_limits},
      // Too many eTLD+1 labels, but foo.com isn't at the end so will be
      // processed.
      {R"({"origins": [
            "https://a.com",
            "https://b.com",
            "https://c.com",
            "https://d.com",
            "https://foo.com",
            "https://e.com"
          ]})",
       ok},
      {R"({"origins": [
            "https://foo.co.uk",
            "https://foo.de",
            "https://foo.in",
            "https://foo.net",
            "https://foo.org",
            "https://foo.com"
          ]})",
       ok},
  };

  for (const auto& test : kTestCases) {
    SCOPED_TRACE(test.json);

    EXPECT_EQ(test.expected, Test("https://foo.com", test.json));
  }
}

TEST_F(WebAuthRequestSecurityCheckerSingleFrameTest,
       ValidateDomainAndRelyingPartyID_CspMetrics) {
  const char kCsp[] = "connect-src https://allowed.com";
  auto navigation = NavigationSimulator::CreateBrowserInitiated(
      GURL("https://example.com"), web_contents());
  navigation->Commit();
  RenderFrameHost* frame = web_contents()->GetPrimaryMainFrame();

  auto policies = network::ParseContentSecurityPolicies(
      kCsp, network::mojom::ContentSecurityPolicyType::kEnforce,
      network::mojom::ContentSecurityPolicySource::kHTTP,
      GURL("https://example.com"));
  static_cast<RenderFrameHostImpl*>(frame)
      ->policy_container_host()
      ->AddContentSecurityPoliciesForTesting(std::move(policies));

  network::TestURLLoaderFactory test_url_loader_factory;
  auto shared_url_loader_factory =
      base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
          &test_url_loader_factory);

  MockWebAuthnContentBrowserClient mock_client;
  mock_client.set_shared_url_loader_factory(shared_url_loader_factory);
  ContentBrowserClient* old_client = SetBrowserClientForTesting(&mock_client);
  WebAuthRequestSecurityCheckerImpl::
      UseSystemSharedURLLoaderFactoryForTesting() = true;

  scoped_refptr<WebAuthRequestSecurityCheckerImpl> checker =
      static_cast<RenderFrameHostImpl*>(frame)
          ->GetWebAuthRequestSecurityCheckerImpl();

  base::HistogramTester histograms;

  // RP ID allowed.
  EXPECT_CALL(mock_client, LogWebFeatureForCurrentPage(_, _)).Times(0);
  base::RunLoop run_loop1;
  auto validation1 = checker->ValidateDomainAndRelyingPartyID(
      url::Origin::Create(GURL("https://example.com")), "allowed.com",
      WebAuthRequestSecurityChecker::RequestType::kGetAssertion,
      /*remote_desktop_client_override=*/std::nullopt,
      base::BindLambdaForTesting(
          [&](blink::mojom::AuthenticatorStatus status) { run_loop1.Quit(); }));
  test_url_loader_factory.AddResponse(
      "https://allowed.com/.well-known/webauthn", "");
  run_loop1.Run();
  histograms.ExpectUniqueSample("WebAuthentication.CspAllow.Remote", true, 1);

  // RP ID disallowed.
  EXPECT_CALL(
      mock_client,
      LogWebFeatureForCurrentPage(
          frame,
          blink::mojom::WebFeature::kWebAuthenticationRemoteCspDisallowsRpId))
      .Times(1);
  base::RunLoop run_loop2;
  auto validation2 = checker->ValidateDomainAndRelyingPartyID(
      url::Origin::Create(GURL("https://example.com")), "disallowed.com",
      WebAuthRequestSecurityChecker::RequestType::kGetAssertion,
      /*remote_desktop_client_override=*/std::nullopt,
      base::BindLambdaForTesting(
          [&](blink::mojom::AuthenticatorStatus status) { run_loop2.Quit(); }));
  test_url_loader_factory.AddResponse(
      "https://disallowed.com/.well-known/webauthn", "");
  run_loop2.Run();
  histograms.ExpectBucketCount("WebAuthentication.CspAllow.Remote", false, 1);
  histograms.ExpectTotalCount("WebAuthentication.CspAllow.Remote", 2);

  WebAuthRequestSecurityCheckerImpl::
      UseSystemSharedURLLoaderFactoryForTesting() = false;
  SetBrowserClientForTesting(old_client);
}

// When `skip_rp_id_validation` is true, RP ID validation is delegated to the
// remote client and no related-origin request is attempted.
TEST_F(WebAuthRequestSecurityCheckerSingleFrameTest,
       ValidateDomainAndRelyingPartyID_DelegatedSkipsRpIdValidation) {
  auto navigation = NavigationSimulator::CreateBrowserInitiated(
      GURL("https://example.com"), web_contents());
  navigation->Commit();
  RenderFrameHost* frame = web_contents()->GetPrimaryMainFrame();

  MockWebAuthnContentBrowserClient mock_client;
  mock_client.web_authentication_delegate.authorized_origin =
      url::Origin::Create(GURL("https://example.com"));
  ContentBrowserClient* old_client = SetBrowserClientForTesting(&mock_client);

  scoped_refptr<WebAuthRequestSecurityCheckerImpl> checker =
      static_cast<RenderFrameHostImpl*>(frame)
          ->GetWebAuthRequestSecurityCheckerImpl();

  // A non-matching RP ID would normally trigger the related-origin request;
  // with delegation it must not.
  EXPECT_CALL(mock_client, LogWebFeatureForCurrentPage(_, _)).Times(0);
  std::optional<blink::mojom::AuthenticatorStatus> result;
  auto validation = checker->ValidateDomainAndRelyingPartyID(
      url::Origin::Create(GURL("https://example.com")), "unrelated.example",
      WebAuthRequestSecurityChecker::RequestType::kGetAssertion,
      WebAuthRequestSecurityChecker::RemoteDesktopParams{
          .origin = url::Origin::Create(GURL("https://remote-host.example")),
          .skip_rp_id_validation = true},
      base::BindLambdaForTesting(
          [&](blink::mojom::AuthenticatorStatus status) { result = status; }));

  // Resolves synchronously to SUCCESS with no pending remote validation.
  EXPECT_EQ(validation, nullptr);
  ASSERT_TRUE(result.has_value());
  EXPECT_EQ(*result, blink::mojom::AuthenticatorStatus::SUCCESS);

  SetBrowserClientForTesting(old_client);
}

TEST_F(WebAuthRequestSecurityCheckerSingleFrameTest,
       ValidateCrossDeviceFallbackUrl) {
  base::test::ScopedFeatureList feature_list(
      device::kWebAuthnCrossDeviceFallbackUrl);
  auto navigation = NavigationSimulator::CreateBrowserInitiated(
      GURL("https://example.com"), web_contents());
  navigation->Commit();
  RenderFrameHost* frame = web_contents()->GetPrimaryMainFrame();

  auto policies = network::ParseContentSecurityPolicies(
      "connect-src https://allowed.com",
      network::mojom::ContentSecurityPolicyType::kEnforce,
      network::mojom::ContentSecurityPolicySource::kHTTP,
      GURL("https://example.com"));
  static_cast<RenderFrameHostImpl*>(frame)
      ->policy_container_host()
      ->AddContentSecurityPoliciesForTesting(std::move(policies));

  scoped_refptr<WebAuthRequestSecurityCheckerImpl> checker =
      static_cast<RenderFrameHostImpl*>(frame)
          ->GetWebAuthRequestSecurityCheckerImpl();

  // 1. Valid GURL, allowed by CSP, matches RP ID.
  EXPECT_TRUE(checker->ValidateCrossDeviceFallbackUrl(
      "allowed.com", GURL("https://allowed.com/fallback")));

  // 2. Blocked by CSP.
  EXPECT_FALSE(checker->ValidateCrossDeviceFallbackUrl(
      "blocked.com", GURL("https://blocked.com/fallback")));

  // 3. Invalid scheme (HTTP).
  EXPECT_FALSE(checker->ValidateCrossDeviceFallbackUrl(
      "allowed.com", GURL("http://allowed.com/fallback")));

  // 4. Invalid GURL.
  EXPECT_FALSE(
      checker->ValidateCrossDeviceFallbackUrl("allowed.com", GURL("")));

  // 5. RP ID mismatch.
  EXPECT_FALSE(checker->ValidateCrossDeviceFallbackUrl(
      "google.com", GURL("https://allowed.com/fallback")));
}

}  // namespace
}  // namespace content
