blob: b93aa984475232ce8ef6b954e1ff940f472c178b [file] [log] [blame]
// Copyright 2017 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 "third_party/blink/renderer/modules/credentialmanager/credentials_container.h"
#include <memory>
#include <utility>
#include "base/macros.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
#include "third_party/blink/public/mojom/credentialmanager/credential_manager.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_gc_controller.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_credential_creation_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_credential_request_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_public_key_credential_creation_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_public_key_credential_parameters.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_public_key_credential_rp_entity.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_public_key_credential_user_entity.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/testing/gc_object_liveness_observer.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h"
#include "third_party/blink/renderer/modules/credentialmanager/credential.h"
#include "third_party/blink/renderer/modules/credentialmanager/credential_manager_proxy.h"
#include "third_party/blink/renderer/modules/credentialmanager/federated_credential.h"
#include "third_party/blink/renderer/modules/credentialmanager/password_credential.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/wrapper_type_info.h"
#include "third_party/blink/renderer/platform/heap/heap_allocator.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
namespace blink {
namespace {
class MockCredentialManager : public mojom::blink::CredentialManager {
public:
MockCredentialManager() {}
~MockCredentialManager() override {}
void Bind(mojo::PendingReceiver<::blink::mojom::blink::CredentialManager>
receiver) {
receiver_.Bind(std::move(receiver));
}
void WaitForConnectionError() {
if (!receiver_.is_bound())
return;
receiver_.set_disconnect_handler(WTF::Bind(&test::ExitRunLoop));
test::EnterRunLoop();
}
void WaitForCallToGet() {
if (get_callback_)
return;
test::EnterRunLoop();
}
void InvokeGetCallback() {
EXPECT_TRUE(receiver_.is_bound());
auto info = blink::mojom::blink::CredentialInfo::New();
info->type = blink::mojom::blink::CredentialType::EMPTY;
info->federation = SecurityOrigin::CreateUniqueOpaque();
std::move(get_callback_)
.Run(blink::mojom::blink::CredentialManagerError::SUCCESS,
std::move(info));
}
protected:
void Store(blink::mojom::blink::CredentialInfoPtr credential,
StoreCallback callback) override {}
void PreventSilentAccess(PreventSilentAccessCallback callback) override {}
void Get(blink::mojom::blink::CredentialMediationRequirement mediation,
bool include_passwords,
const WTF::Vector<::blink::KURL>& federations,
GetCallback callback) override {
get_callback_ = std::move(callback);
test::ExitRunLoop();
}
private:
mojo::Receiver<::blink::mojom::blink::CredentialManager> receiver_{this};
GetCallback get_callback_;
DISALLOW_COPY_AND_ASSIGN(MockCredentialManager);
};
class CredentialManagerTestingContext {
STACK_ALLOCATED();
public:
CredentialManagerTestingContext(
MockCredentialManager* mock_credential_manager)
: dummy_context_(KURL("https://example.test")) {
dummy_context_.GetDocument().SetSecureContextModeForTesting(
SecureContextMode::kSecureContext);
dummy_context_.GetFrame().GetBrowserInterfaceBroker().SetBinderForTesting(
::blink::mojom::blink::CredentialManager::Name_,
WTF::BindRepeating(
[](MockCredentialManager* mock_credential_manager,
mojo::ScopedMessagePipeHandle handle) {
mock_credential_manager->Bind(
mojo::PendingReceiver<
::blink::mojom::blink::CredentialManager>(
std::move(handle)));
},
WTF::Unretained(mock_credential_manager)));
}
~CredentialManagerTestingContext() {
dummy_context_.GetFrame().GetBrowserInterfaceBroker().SetBinderForTesting(
::blink::mojom::blink::CredentialManager::Name_, {});
}
Document* GetDocument() { return &dummy_context_.GetDocument(); }
LocalFrame* Frame() { return &dummy_context_.GetFrame(); }
ScriptState* GetScriptState() { return dummy_context_.GetScriptState(); }
private:
V8TestingScope dummy_context_;
};
} // namespace
class MockPublicKeyCredential : public Credential {
public:
MockPublicKeyCredential() : Credential("test", "public-key") {}
bool IsPublicKeyCredential() const override { return true; }
};
// The completion callbacks for pending mojom::CredentialManager calls each own
// a persistent handle to a ScriptPromiseResolver instance. Ensure that if the
// document is destored while a call is pending, it can still be freed up.
TEST(CredentialsContainerTest, PendingGetRequest_NoGCCycles) {
MockCredentialManager mock_credential_manager;
GCObjectLivenessObserver<Document> document_observer;
{
CredentialManagerTestingContext context(&mock_credential_manager);
document_observer.Observe(context.GetDocument());
MakeGarbageCollected<CredentialsContainer>()->get(
context.GetScriptState(), CredentialRequestOptions::Create());
mock_credential_manager.WaitForCallToGet();
}
V8GCController::CollectAllGarbageForTesting(
v8::Isolate::GetCurrent(),
v8::EmbedderHeapTracer::EmbedderStackState::kEmpty);
ThreadState::Current()->CollectAllGarbageForTesting();
ASSERT_TRUE(document_observer.WasCollected());
mock_credential_manager.InvokeGetCallback();
mock_credential_manager.WaitForConnectionError();
}
// If the document is detached before the request is resolved, the promise
// should be left unresolved, and there should be no crashes.
TEST(CredentialsContainerTest,
PendingGetRequest_NoCrashOnResponseAfterDocumentShutdown) {
MockCredentialManager mock_credential_manager;
CredentialManagerTestingContext context(&mock_credential_manager);
auto* proxy = CredentialManagerProxy::From(*context.GetDocument());
auto promise = MakeGarbageCollected<CredentialsContainer>()->get(
context.GetScriptState(), CredentialRequestOptions::Create());
mock_credential_manager.WaitForCallToGet();
context.GetDocument()->Shutdown();
mock_credential_manager.InvokeGetCallback();
proxy->FlushCredentialManagerConnectionForTesting();
EXPECT_EQ(v8::Promise::kPending,
promise.V8Value().As<v8::Promise>()->State());
}
TEST(CredentialsContainerTest, RejectPublicKeyCredentialStoreOperation) {
MockCredentialManager mock_credential_manager;
CredentialManagerTestingContext context(&mock_credential_manager);
auto promise = MakeGarbageCollected<CredentialsContainer>()->store(
context.GetScriptState(),
MakeGarbageCollected<MockPublicKeyCredential>());
EXPECT_EQ(v8::Promise::kRejected,
promise.V8Value().As<v8::Promise>()->State());
}
TEST(CredentialsContainerTest,
RejectStoringPasswordCredentialWithInvalidIconURL) {
MockCredentialManager mock_credential_manager;
CredentialManagerTestingContext context(&mock_credential_manager);
KURL invalid_url("an invalid URL");
auto* credential = MakeGarbageCollected<PasswordCredential>(
"id", "password", "name", invalid_url);
auto promise = MakeGarbageCollected<CredentialsContainer>()->store(
context.GetScriptState(), credential);
auto v8promise = promise.V8Value().As<v8::Promise>();
EXPECT_EQ(v8::Promise::kRejected, v8promise->State());
auto* exception = ToScriptWrappable(v8promise->Result().As<v8::Object>())
->ToImpl<DOMException>();
EXPECT_EQ("SecurityError", exception->name());
EXPECT_EQ("'iconURL' should be a secure URL", exception->message());
}
TEST(CredentialsContainerTest,
RejectStoringFederatedCredentialWithInvalidIconURL) {
MockCredentialManager mock_credential_manager;
CredentialManagerTestingContext context(&mock_credential_manager);
KURL invalid_url("an invalid URL");
auto origin = SecurityOrigin::CreateFromString("https://example.test");
auto* credential = MakeGarbageCollected<FederatedCredential>(
"id", origin, "name", invalid_url);
auto promise = MakeGarbageCollected<CredentialsContainer>()->store(
context.GetScriptState(), credential);
auto v8promise = promise.V8Value().As<v8::Promise>();
EXPECT_EQ(v8::Promise::kRejected, v8promise->State());
auto* exception = ToScriptWrappable(v8promise->Result().As<v8::Object>())
->ToImpl<DOMException>();
EXPECT_EQ("SecurityError", exception->name());
EXPECT_EQ("'iconURL' should be a secure URL", exception->message());
}
TEST(CredentialsContainerTest,
RejectCreatingPublicKeyCredentialWithInvalidIconURL) {
MockCredentialManager mock_credential_manager;
CredentialManagerTestingContext context(&mock_credential_manager);
auto* rp_options = PublicKeyCredentialRpEntity::Create();
rp_options->setId("example.test");
rp_options->setName("Example RP");
auto* user_options = PublicKeyCredentialUserEntity::Create();
int dummy_buffer_source = 1;
auto dummy_buffer =
ArrayBufferOrArrayBufferView::FromArrayBuffer(DOMArrayBuffer::Create(
&dummy_buffer_source, sizeof(dummy_buffer_source)));
user_options->setId(dummy_buffer);
user_options->setIcon("invalid URL");
auto* public_key_options = PublicKeyCredentialCreationOptions::Create();
public_key_options->setChallenge(dummy_buffer);
public_key_options->setUser(user_options);
public_key_options->setRp(rp_options);
public_key_options->setAttestation("none");
auto* public_key_param = PublicKeyCredentialParameters::Create();
public_key_param->setAlg(1);
public_key_param->setType("public-key");
auto params = HeapVector<Member<PublicKeyCredentialParameters>>();
params.push_back(public_key_param);
public_key_options->setPubKeyCredParams(params);
auto* credential_options = CredentialCreationOptions::Create();
credential_options->setPublicKey(public_key_options);
auto promise = MakeGarbageCollected<CredentialsContainer>()->create(
context.GetScriptState(), credential_options,
IGNORE_EXCEPTION_FOR_TESTING);
auto v8promise = promise.V8Value().As<v8::Promise>();
EXPECT_EQ(v8::Promise::kRejected, v8promise->State());
auto* exception = ToScriptWrappable(v8promise->Result().As<v8::Object>())
->ToImpl<DOMException>();
EXPECT_EQ("SecurityError", exception->name());
EXPECT_EQ("'user.icon' should be a secure URL", exception->message());
}
} // namespace blink