blob: 36bf62be4782c6e625bc04dc51f2974c07b93588 [file] [log] [blame]
// Copyright 2014 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 "modules/credentialmanager/CredentialsContainer.h"
#include <memory>
#include <utility>
#include "bindings/core/v8/ExceptionState.h"
#include "bindings/core/v8/ScriptPromise.h"
#include "bindings/core/v8/ScriptPromiseResolver.h"
#include "core/dom/DOMException.h"
#include "core/dom/Document.h"
#include "core/dom/ExceptionCode.h"
#include "core/dom/ExecutionContext.h"
#include "core/frame/Frame.h"
#include "core/frame/UseCounter.h"
#include "core/inspector/ConsoleMessage.h"
#include "core/page/FrameTree.h"
#include "core/typed_arrays/DOMArrayBuffer.h"
#include "modules/credentialmanager/AuthenticatorAssertionResponse.h"
#include "modules/credentialmanager/AuthenticatorAttestationResponse.h"
#include "modules/credentialmanager/Credential.h"
#include "modules/credentialmanager/CredentialCreationOptions.h"
#include "modules/credentialmanager/CredentialManagerProxy.h"
#include "modules/credentialmanager/CredentialManagerTypeConverters.h"
#include "modules/credentialmanager/CredentialRequestOptions.h"
#include "modules/credentialmanager/FederatedCredential.h"
#include "modules/credentialmanager/FederatedCredentialRequestOptions.h"
#include "modules/credentialmanager/PasswordCredential.h"
#include "modules/credentialmanager/PublicKeyCredential.h"
#include "modules/credentialmanager/PublicKeyCredentialCreationOptions.h"
#include "modules/credentialmanager/PublicKeyCredentialRequestOptions.h"
#include "platform/weborigin/OriginAccessEntry.h"
#include "platform/weborigin/SecurityOrigin.h"
#include "platform/wtf/Functional.h"
#include "third_party/WebKit/public/platform/modules/credentialmanager/credential_manager.mojom-blink.h"
namespace blink {
namespace {
using ::password_manager::mojom::blink::CredentialManagerError;
using ::password_manager::mojom::blink::CredentialInfo;
using ::password_manager::mojom::blink::CredentialInfoPtr;
using ::password_manager::mojom::blink::CredentialMediationRequirement;
using ::webauth::mojom::blink::AuthenticatorStatus;
using MojoPublicKeyCredentialCreationOptions =
::webauth::mojom::blink::PublicKeyCredentialCreationOptions;
using ::webauth::mojom::blink::MakeCredentialAuthenticatorResponsePtr;
using MojoPublicKeyCredentialRequestOptions =
::webauth::mojom::blink::PublicKeyCredentialRequestOptions;
using ::webauth::mojom::blink::GetAssertionAuthenticatorResponsePtr;
enum class RequiredOriginType { kSecure, kSecureAndSameWithAncestors };
// Off-heap wrapper that holds a strong reference to a ScriptPromiseResolver.
class ScopedPromiseResolver {
WTF_MAKE_NONCOPYABLE(ScopedPromiseResolver);
public:
explicit ScopedPromiseResolver(ScriptPromiseResolver* resolver)
: resolver_(resolver) {}
~ScopedPromiseResolver() {
if (resolver_)
OnConnectionError();
}
// Releases the owned |resolver_|. This is to be called by the Mojo response
// callback responsible for resolving the corresponding ScriptPromise.
//
// If this method is not called before |this| goes of scope, it is assumed
// that a Mojo connection error has occurred, and the response callback was
// never invoked. The Promise will be rejected with an appropriate exception.
ScriptPromiseResolver* Release() { return resolver_.Release(); }
private:
void OnConnectionError() {
// The only anticapted reason for a connection error is that the embedder
// does not implement mojom::CredentialManager, so go out on a limb and try
// to provide an actionable error message.
resolver_->Reject(DOMException::Create(
kNotSupportedError,
"The user agent does not implement a password store."));
}
Persistent<ScriptPromiseResolver> resolver_;
};
bool IsSameOriginWithAncestors(const Frame* frame) {
DCHECK(frame);
const Frame* current = frame;
const SecurityOrigin* origin =
frame->GetSecurityContext()->GetSecurityOrigin();
while (current->Tree().Parent()) {
current = current->Tree().Parent();
if (!origin->CanAccess(current->GetSecurityContext()->GetSecurityOrigin()))
return false;
}
return true;
}
bool CheckSecurityRequirementsBeforeRequest(
ScriptPromiseResolver* resolver,
RequiredOriginType required_origin_type) {
// Ignore calls if the current realm execution context is no longer valid,
// e.g., because the responsible document was detached.
DCHECK(resolver->GetExecutionContext());
if (resolver->GetExecutionContext()->IsContextDestroyed()) {
resolver->Reject();
return false;
}
// The API is not exposed to Workers or Worklets, so if the current realm
// execution context is valid, it must have a responsible browsing context.
SECURITY_CHECK(resolver->GetFrame());
String error_message;
if (!resolver->GetExecutionContext()->IsSecureContext(error_message)) {
resolver->Reject(DOMException::Create(kSecurityError, error_message));
return false;
}
if (required_origin_type == RequiredOriginType::kSecureAndSameWithAncestors &&
!IsSameOriginWithAncestors(resolver->GetFrame())) {
resolver->Reject(DOMException::Create(
kNotAllowedError,
"`PasswordCredential` and `FederatedCredential` objects may only be "
"stored/retrieved in a document which is same-origin with all of its "
"ancestors."));
return false;
}
return true;
}
void AssertSecurityRequirementsBeforeResponse(
ScriptPromiseResolver* resolver,
RequiredOriginType require_origin) {
// The |resolver| will blanket ignore Reject/Resolve calls if the context is
// gone -- nevertheless, call Reject() to be on the safe side.
if (!resolver->GetExecutionContext()) {
resolver->Reject();
return;
}
SECURITY_CHECK(resolver->GetFrame());
SECURITY_CHECK(resolver->GetExecutionContext()->IsSecureContext());
SECURITY_CHECK(require_origin !=
RequiredOriginType::kSecureAndSameWithAncestors ||
IsSameOriginWithAncestors(resolver->GetFrame()));
}
bool CheckPublicKeySecurityRequirements(ScriptPromiseResolver* resolver,
const String& relying_party_id) {
const SecurityOrigin* origin =
resolver->GetFrame()->GetSecurityContext()->GetSecurityOrigin();
if (origin->IsUnique()) {
String error_message =
"The origin ' " + origin->ToRawString() +
"' is an opaque origin and hence not allowed to access " +
"'PublicKeyCredential' objects.";
resolver->Reject(DOMException::Create(kNotAllowedError, error_message));
return false;
}
DCHECK_NE(origin->Protocol(), url::kAboutScheme);
DCHECK_NE(origin->Protocol(), url::kFileScheme);
// Validate the effective domain.
// For step 6 of both
// https://w3c.github.io/webauthn/#createCredential and
// https://w3c.github.io/webauthn/#discover-from-external-source.
String effective_domain = origin->Domain();
// TODO(crbug.com/803077): Avoid constructing an OriginAccessEntry just
// for the IP address check.
OriginAccessEntry access_entry(origin->Protocol(), effective_domain,
blink::OriginAccessEntry::kAllowSubdomains);
if (effective_domain.IsEmpty() || access_entry.HostIsIPAddress()) {
resolver->Reject(DOMException::Create(
kSecurityError, "Effective domain is not a valid domain."));
return false;
}
// For the steps detailed in
// https://w3c.github.io/webauthn/#CreateCred-DetermineRpId and
// https://w3c.github.io/webauthn/#GetAssn-DetermineRpId.
if (!relying_party_id.IsNull()) {
OriginAccessEntry access_entry(origin->Protocol(), relying_party_id,
blink::OriginAccessEntry::kAllowSubdomains);
if (access_entry.MatchesDomain(*origin) !=
blink::OriginAccessEntry::kMatchesOrigin) {
resolver->Reject(DOMException::Create(
kSecurityError,
"The relying party ID '" + relying_party_id +
"' is not a registrable domain suffix of, nor equal to '" +
origin->ToRawString() + "'."));
return false;
}
}
return true;
}
// Checks if the icon URL of |credential| is an a-priori authenticated URL.
// https://w3c.github.io/webappsec-credential-management/#dom-credentialuserdata-iconurl
bool IsIconURLEmptyOrSecure(const Credential* credential) {
if (!credential->IsPasswordCredential() &&
!credential->IsFederatedCredential()) {
DCHECK(credential->IsPublicKeyCredential());
return true;
}
const KURL& url =
credential->IsFederatedCredential()
? static_cast<const FederatedCredential*>(credential)->iconURL()
: static_cast<const PasswordCredential*>(credential)->iconURL();
if (url.IsEmpty())
return true;
// https://www.w3.org/TR/mixed-content/#a-priori-authenticated-url
return url.IsAboutSrcdocURL() || url.IsAboutBlankURL() ||
url.ProtocolIsData() ||
SecurityOrigin::Create(url)->IsPotentiallyTrustworthy();
}
DOMException* CredentialManagerErrorToDOMException(
CredentialManagerError reason) {
switch (reason) {
case CredentialManagerError::PENDING_REQUEST:
return DOMException::Create(kInvalidStateError,
"A request is already pending.");
case CredentialManagerError::PASSWORD_STORE_UNAVAILABLE:
return DOMException::Create(kNotSupportedError,
"The password store is unavailable.");
case CredentialManagerError::NOT_ALLOWED:
return DOMException::Create(kNotAllowedError,
"The operation is not allowed.");
case CredentialManagerError::NOT_SUPPORTED:
return DOMException::Create(
kNotSupportedError,
"Parameters for this operation are not supported.");
case CredentialManagerError::INVALID_DOMAIN:
return DOMException::Create(kSecurityError, "This is an invalid domain.");
case CredentialManagerError::TIMED_OUT:
return DOMException::Create(kNotAllowedError, "Operation timed out.");
case CredentialManagerError::NOT_IMPLEMENTED:
return DOMException::Create(kNotSupportedError, "Not implemented");
case CredentialManagerError::UNKNOWN:
return DOMException::Create(kNotReadableError,
"An unknown error occurred while talking "
"to the credential manager.");
case CredentialManagerError::SUCCESS:
NOTREACHED();
break;
}
return nullptr;
}
void OnStoreComplete(std::unique_ptr<ScopedPromiseResolver> scoped_resolver,
RequiredOriginType required_origin_type) {
auto* resolver = scoped_resolver->Release();
AssertSecurityRequirementsBeforeResponse(resolver, required_origin_type);
resolver->Resolve();
}
void OnPreventSilentAccessComplete(
std::unique_ptr<ScopedPromiseResolver> scoped_resolver) {
auto* resolver = scoped_resolver->Release();
const auto required_origin_type = RequiredOriginType::kSecure;
AssertSecurityRequirementsBeforeResponse(resolver, required_origin_type);
resolver->Resolve();
}
void OnGetComplete(std::unique_ptr<ScopedPromiseResolver> scoped_resolver,
RequiredOriginType required_origin_type,
CredentialManagerError error,
CredentialInfoPtr credential_info) {
auto* resolver = scoped_resolver->Release();
AssertSecurityRequirementsBeforeResponse(resolver, required_origin_type);
if (error == CredentialManagerError::SUCCESS) {
DCHECK(credential_info);
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kCredentialManagerGetReturnedCredential);
resolver->Resolve(mojo::ConvertTo<Credential*>(std::move(credential_info)));
} else {
DCHECK(!credential_info);
resolver->Reject(CredentialManagerErrorToDOMException(error));
}
}
DOMArrayBuffer* VectorToDOMArrayBuffer(const Vector<uint8_t> buffer) {
return DOMArrayBuffer::Create(static_cast<const void*>(buffer.data()),
buffer.size());
}
void OnMakePublicKeyCredentialComplete(
std::unique_ptr<ScopedPromiseResolver> scoped_resolver,
AuthenticatorStatus status,
MakeCredentialAuthenticatorResponsePtr credential) {
auto* resolver = scoped_resolver->Release();
const auto required_origin_type = RequiredOriginType::kSecure;
// TODO(crbug.com/803080): Introduce the assert counterpart of
// CheckPublicKeySecurityRequirements().
AssertSecurityRequirementsBeforeResponse(resolver, required_origin_type);
if (status == AuthenticatorStatus::SUCCESS) {
DCHECK(credential);
DCHECK(!credential->info->client_data_json.IsEmpty());
DCHECK(!credential->attestation_object.IsEmpty());
DOMArrayBuffer* client_data_buffer =
VectorToDOMArrayBuffer(std::move(credential->info->client_data_json));
DOMArrayBuffer* raw_id =
VectorToDOMArrayBuffer(std::move(credential->info->raw_id));
DOMArrayBuffer* attestation_buffer =
VectorToDOMArrayBuffer(std::move(credential->attestation_object));
AuthenticatorAttestationResponse* authenticator_response =
AuthenticatorAttestationResponse::Create(client_data_buffer,
attestation_buffer);
resolver->Resolve(PublicKeyCredential::Create(credential->info->id, raw_id,
authenticator_response));
} else {
DCHECK(!credential);
resolver->Reject(CredentialManagerErrorToDOMException(
mojo::ConvertTo<CredentialManagerError>(status)));
}
}
void OnGetAssertionComplete(
std::unique_ptr<ScopedPromiseResolver> scoped_resolver,
AuthenticatorStatus status,
GetAssertionAuthenticatorResponsePtr credential) {
auto resolver = scoped_resolver->Release();
const auto required_origin_type = RequiredOriginType::kSecure;
AssertSecurityRequirementsBeforeResponse(resolver, required_origin_type);
if (status == AuthenticatorStatus::SUCCESS) {
DCHECK(credential);
DCHECK(!credential->signature.IsEmpty());
DCHECK(!credential->authenticator_data.IsEmpty());
DOMArrayBuffer* client_data_buffer =
VectorToDOMArrayBuffer(std::move(credential->info->client_data_json));
DOMArrayBuffer* raw_id =
VectorToDOMArrayBuffer(std::move(credential->info->raw_id));
DOMArrayBuffer* authenticator_buffer =
VectorToDOMArrayBuffer(std::move(credential->authenticator_data));
DOMArrayBuffer* signature_buffer =
VectorToDOMArrayBuffer(std::move(credential->signature));
DOMArrayBuffer* user_handle =
credential->user_handle
? VectorToDOMArrayBuffer(std::move(*credential->user_handle))
: DOMArrayBuffer::Create(nullptr, 0);
AuthenticatorAssertionResponse* authenticator_response =
AuthenticatorAssertionResponse::Create(client_data_buffer,
authenticator_buffer,
signature_buffer, user_handle);
resolver->Resolve(PublicKeyCredential::Create(credential->info->id, raw_id,
authenticator_response));
} else {
DCHECK(!credential);
resolver->Reject(CredentialManagerErrorToDOMException(
mojo::ConvertTo<CredentialManagerError>(status)));
}
}
} // namespace
CredentialsContainer* CredentialsContainer::Create() {
return new CredentialsContainer();
}
CredentialsContainer::CredentialsContainer() = default;
ScriptPromise CredentialsContainer::get(
ScriptState* script_state,
const CredentialRequestOptions& options) {
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
ScriptPromise promise = resolver->Promise();
auto required_origin_type =
options.hasFederated() || options.hasPassword()
? RequiredOriginType::kSecureAndSameWithAncestors
: RequiredOriginType::kSecure;
if (!CheckSecurityRequirementsBeforeRequest(resolver, required_origin_type))
return promise;
if (options.hasPublicKey()) {
auto mojo_options =
MojoPublicKeyCredentialRequestOptions::From(options.publicKey());
if (mojo_options) {
auto* authenticator =
CredentialManagerProxy::From(script_state)->Authenticator();
authenticator->GetAssertion(
std::move(mojo_options),
WTF::Bind(
&OnGetAssertionComplete,
WTF::Passed(std::make_unique<ScopedPromiseResolver>(resolver))));
} else {
resolver->Reject(DOMException::Create(
kNotSupportedError,
"Required parameters missing in 'options.publicKey'."));
}
return promise;
}
Vector<KURL> providers;
if (options.hasFederated() && options.federated().hasProviders()) {
for (const auto& string : options.federated().providers()) {
KURL url = KURL(NullURL(), string);
if (url.IsValid())
providers.push_back(std::move(url));
}
}
CredentialMediationRequirement requirement;
if (options.mediation() == "silent") {
UseCounter::Count(ExecutionContext::From(script_state),
WebFeature::kCredentialManagerGetMediationSilent);
requirement = CredentialMediationRequirement::kSilent;
} else if (options.mediation() == "optional") {
UseCounter::Count(ExecutionContext::From(script_state),
WebFeature::kCredentialManagerGetMediationOptional);
requirement = CredentialMediationRequirement::kOptional;
} else {
DCHECK_EQ("required", options.mediation());
UseCounter::Count(ExecutionContext::From(script_state),
WebFeature::kCredentialManagerGetMediationRequired);
requirement = CredentialMediationRequirement::kRequired;
}
auto* credential_manager =
CredentialManagerProxy::From(script_state)->CredentialManager();
credential_manager->Get(
requirement, options.password(), std::move(providers),
WTF::Bind(&OnGetComplete,
WTF::Passed(std::make_unique<ScopedPromiseResolver>(resolver)),
required_origin_type));
return promise;
}
ScriptPromise CredentialsContainer::store(ScriptState* script_state,
Credential* credential) {
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
ScriptPromise promise = resolver->Promise();
auto required_origin_type =
credential->IsFederatedCredential() || credential->IsPasswordCredential()
? RequiredOriginType::kSecureAndSameWithAncestors
: RequiredOriginType::kSecure;
if (!CheckSecurityRequirementsBeforeRequest(resolver, required_origin_type))
return promise;
if (!(credential->IsFederatedCredential() ||
credential->IsPasswordCredential())) {
resolver->Reject(DOMException::Create(
kNotSupportedError,
"Store operation not permitted for PublicKey credentials."));
}
if (!IsIconURLEmptyOrSecure(credential)) {
resolver->Reject(DOMException::Create(kSecurityError,
"'iconURL' should be a secure URL"));
return promise;
}
auto* credential_manager =
CredentialManagerProxy::From(script_state)->CredentialManager();
credential_manager->Store(
CredentialInfo::From(credential),
WTF::Bind(&OnStoreComplete,
WTF::Passed(std::make_unique<ScopedPromiseResolver>(resolver)),
required_origin_type));
return promise;
}
ScriptPromise CredentialsContainer::create(
ScriptState* script_state,
const CredentialCreationOptions& options,
ExceptionState& exception_state) {
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
ScriptPromise promise = resolver->Promise();
const auto required_origin_type = RequiredOriginType::kSecure;
if (!CheckSecurityRequirementsBeforeRequest(resolver, required_origin_type))
return promise;
if ((options.hasPassword() + options.hasFederated() +
options.hasPublicKey()) != 1) {
resolver->Reject(DOMException::Create(
kNotSupportedError,
"Only exactly one of 'password', 'federated', and 'publicKey' "
"credential types are currently supported."));
return promise;
}
if (options.hasPassword()) {
resolver->Resolve(
options.password().IsPasswordCredentialData()
? PasswordCredential::Create(
options.password().GetAsPasswordCredentialData(),
exception_state)
: PasswordCredential::Create(
options.password().GetAsHTMLFormElement(), exception_state));
} else if (options.hasFederated()) {
resolver->Resolve(
FederatedCredential::Create(options.federated(), exception_state));
} else {
DCHECK(options.hasPublicKey());
const String& relying_party_id = options.publicKey().rp().id();
if (!CheckPublicKeySecurityRequirements(resolver, relying_party_id)) {
return promise;
}
auto mojo_options =
MojoPublicKeyCredentialCreationOptions::From(options.publicKey());
if (mojo_options) {
if (!mojo_options->relying_party->id) {
mojo_options->relying_party->id = resolver->GetFrame()
->GetSecurityContext()
->GetSecurityOrigin()
->Domain();
}
auto* authenticator =
CredentialManagerProxy::From(script_state)->Authenticator();
authenticator->MakeCredential(
std::move(mojo_options),
WTF::Bind(
&OnMakePublicKeyCredentialComplete,
WTF::Passed(std::make_unique<ScopedPromiseResolver>(resolver))));
} else {
resolver->Reject(DOMException::Create(
kNotSupportedError,
"Required parameters missing in `options.publicKey`."));
}
}
return promise;
}
ScriptPromise CredentialsContainer::preventSilentAccess(
ScriptState* script_state) {
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
ScriptPromise promise = resolver->Promise();
const auto required_origin_type = RequiredOriginType::kSecure;
if (!CheckSecurityRequirementsBeforeRequest(resolver, required_origin_type))
return promise;
auto* credential_manager =
CredentialManagerProxy::From(script_state)->CredentialManager();
credential_manager->PreventSilentAccess(WTF::Bind(
&OnPreventSilentAccessComplete,
WTF::Passed(std::make_unique<ScopedPromiseResolver>(resolver))));
return promise;
}
} // namespace blink