blob: 3283b7ca63d2add91437a3cb90185c4208686223 [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 "third_party/blink/renderer/modules/credentialmanager/credentials_container.h"
#include <memory>
#include <utility>
#include "build/build_config.h"
#include "third_party/blink/public/common/sms/sms_receiver_outcome.h"
#include "third_party/blink/public/mojom/credentialmanager/credential_manager.mojom-blink.h"
#include "third_party/blink/public/mojom/feature_policy/feature_policy.mojom-blink.h"
#include "third_party/blink/public/mojom/sms/sms_receiver.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_client_inputs.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authenticator_selection_criteria.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_federated_credential_request_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_otp_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_request_options.h"
#include "third_party/blink/renderer/core/dom/abort_signal.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/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/frame.h"
#include "third_party/blink/renderer/core/frame/frame_console.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/page/frame_tree.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h"
#include "third_party/blink/renderer/modules/credentialmanager/authenticator_assertion_response.h"
#include "third_party/blink/renderer/modules/credentialmanager/authenticator_attestation_response.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/credential_manager_type_converters.h"
#include "third_party/blink/renderer/modules/credentialmanager/credential_metrics.h"
#include "third_party/blink/renderer/modules/credentialmanager/federated_credential.h"
#include "third_party/blink/renderer/modules/credentialmanager/otp_credential.h"
#include "third_party/blink/renderer/modules/credentialmanager/password_credential.h"
#include "third_party/blink/renderer/modules/credentialmanager/public_key_credential.h"
#include "third_party/blink/renderer/modules/credentialmanager/scoped_promise_resolver.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/weborigin/origin_access_entry.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#if defined(OS_ANDROID)
#include "third_party/blink/renderer/bindings/modules/v8/v8_public_key_credential_rp_entity.h"
#endif
namespace blink {
namespace {
using mojom::blink::CredentialManagerError;
using mojom::blink::CredentialInfo;
using mojom::blink::CredentialInfoPtr;
using mojom::blink::CredentialMediationRequirement;
using mojom::blink::AuthenticatorStatus;
using MojoPublicKeyCredentialCreationOptions =
mojom::blink::PublicKeyCredentialCreationOptions;
using mojom::blink::MakeCredentialAuthenticatorResponsePtr;
using MojoPublicKeyCredentialRequestOptions =
mojom::blink::PublicKeyCredentialRequestOptions;
using mojom::blink::GetAssertionAuthenticatorResponsePtr;
constexpr char kCryptotokenOrigin[] =
"chrome-extension://kmendfapggjehodndflmmgagdbamhnfd";
// RequiredOriginType enumerates the requirements on the environment to perform
// an operation.
enum class RequiredOriginType {
// Must be a secure origin.
kSecure,
// Must be a secure origin and be same-origin with all ancestor frames.
kSecureAndSameWithAncestors,
// Must be a secure origin and the "publickey-credentials" feature policy
// must be enabled. By default "publickey-credentials" is not inherited by
// cross-origin child frames, so if that policy is not explicitly enabled,
// behavior is the same as that of |kSecureAndSameWithAncestors|. Note that
// feature policies can be expressed in various ways, e.g.: |allow| iframe
// attribute and/or feature-policy header, and may be inherited from parent
// browsing contexts. See Feature Policy spec.
kSecureAndPermittedByFeaturePolicy,
};
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->IsSameOriginWith(
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());
// The API is not exposed in non-secure context.
SECURITY_CHECK(resolver->GetExecutionContext()->IsSecureContext());
switch (required_origin_type) {
case RequiredOriginType::kSecure:
// This has already been checked.
break;
case RequiredOriginType::kSecureAndSameWithAncestors:
if (!IsSameOriginWithAncestors(resolver->GetFrame())) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"The following credential operations can only occur in a document "
"which is same-origin with all of its ancestors: storage/retrieval "
"of 'PasswordCredential' and 'FederatedCredential'."));
return false;
}
break;
case RequiredOriginType::kSecureAndPermittedByFeaturePolicy:
// The 'publickey-credentials' feature's "default allowlist" is "self",
// which means the webauthn feature is allowed by default in same-origin
// child browsing contexts.
if (!resolver->GetFrame()->GetSecurityContext()->IsFeatureEnabled(
mojom::blink::FeaturePolicyFeature::kPublicKeyCredentials)) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"The 'publickey-credentials' feature is not enabled in this "
"document. Feature Policy may be used to delegate Web "
"Authentication capabilities to cross-origin child frames."));
return false;
}
break;
}
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());
switch (require_origin) {
case RequiredOriginType::kSecure:
// This has already been checked.
break;
case RequiredOriginType::kSecureAndSameWithAncestors:
SECURITY_CHECK(IsSameOriginWithAncestors(resolver->GetFrame()));
break;
case RequiredOriginType::kSecureAndPermittedByFeaturePolicy:
SECURITY_CHECK(
resolver->GetFrame()->GetSecurityContext()->IsFeatureEnabled(
mojom::blink::FeaturePolicyFeature::kPublicKeyCredentials));
break;
}
}
// Checks if the icon URL is an a-priori authenticated URL.
// https://w3c.github.io/webappsec-credential-management/#dom-credentialuserdata-iconurl
bool IsIconURLNullOrSecure(const KURL& url) {
if (url.IsNull())
return true;
if (!url.IsValid())
return false;
// 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 MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError,
"A request is already pending.");
case CredentialManagerError::PASSWORD_STORE_UNAVAILABLE:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"The password store is unavailable.");
case CredentialManagerError::NOT_ALLOWED:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"The operation either timed out or was not allowed. See: "
"https://w3c.github.io/webauthn/#sec-assertion-privacy.");
case CredentialManagerError::INVALID_DOMAIN:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError, "This is an invalid domain.");
case CredentialManagerError::INVALID_ICON_URL:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError, "The icon should be a secure URL");
case CredentialManagerError::CREDENTIAL_EXCLUDED:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError,
"The user attempted to register an authenticator that contains one "
"of the credentials already registered with the relying party.");
case CredentialManagerError::CREDENTIAL_NOT_RECOGNIZED:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError,
"The user attempted to use an authenticator "
"that recognized none of the provided "
"credentials.");
case CredentialManagerError::NOT_IMPLEMENTED:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError, "Not implemented");
case CredentialManagerError::NOT_FOCUSED:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"The operation is not allowed at this time "
"because the page does not have focus.");
case CredentialManagerError::RESIDENT_CREDENTIALS_UNSUPPORTED:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Resident credentials or empty "
"'allowCredentials' lists are not supported "
"at this time.");
case CredentialManagerError::PROTECTION_POLICY_INCONSISTENT:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Requested protection policy is inconsistent or incongruent with "
"other requested parameters.");
case CredentialManagerError::ANDROID_ALGORITHM_UNSUPPORTED:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"None of the algorithms specified in "
"`pubKeyCredParams` are supported by "
"this device.");
case CredentialManagerError::ANDROID_EMPTY_ALLOW_CREDENTIALS:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Use of an empty `allowCredentials` list is "
"not supported on this device.");
case CredentialManagerError::ANDROID_NOT_SUPPORTED_ERROR:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Either the device has received unexpected "
"request parameters, or the device "
"cannot support this request.");
case CredentialManagerError::ANDROID_USER_VERIFICATION_UNSUPPORTED:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"The specified `userVerification` "
"requirement cannot be fulfilled by "
"this device unless the device is secured "
"with a screen lock.");
case CredentialManagerError::ABORT:
return MakeGarbageCollected<DOMException>(DOMExceptionCode::kAbortError,
"Request has been aborted.");
case CredentialManagerError::OPAQUE_DOMAIN:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"The current origin is an opaque origin and hence not allowed to "
"access 'PublicKeyCredential' objects.");
case CredentialManagerError::INVALID_PROTOCOL:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError,
"Public-key credentials are only available to HTTPS origin or HTTP "
"origins that fall under 'localhost'. See https://crbug.com/824383");
case CredentialManagerError::BAD_RELYING_PARTY_ID:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError,
"The relying party ID is not a registrable domain suffix of, nor "
"equal to the current domain.");
case CredentialManagerError::UNKNOWN:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotReadableError,
"An unknown error occurred while talking "
"to the credential manager.");
case CredentialManagerError::SUCCESS:
NOTREACHED();
break;
}
return nullptr;
}
// Abort an ongoing PublicKeyCredential create() or get() operation.
void AbortPublicKeyRequest(ScriptState* script_state) {
if (!script_state->ContextIsValid())
return;
auto* authenticator =
CredentialManagerProxy::From(script_state)->Authenticator();
authenticator->Cancel();
}
// Abort an ongoing OtpCredential get() operation.
void AbortOtpRequest(ScriptState* script_state) {
if (!script_state->ContextIsValid())
return;
auto* sms_receiver =
CredentialManagerProxy::From(script_state)->SmsReceiver();
sms_receiver->Abort();
}
void OnStoreComplete(std::unique_ptr<ScopedPromiseResolver> scoped_resolver) {
auto* resolver = scoped_resolver->Release();
AssertSecurityRequirementsBeforeResponse(
resolver, RequiredOriginType::kSecureAndSameWithAncestors);
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());
}
#if defined(OS_ANDROID)
Vector<Vector<uint32_t>> UvmEntryToArray(
const Vector<mojom::blink::UvmEntryPtr>& user_verification_methods) {
Vector<Vector<uint32_t>> uvm_array;
for (const auto& uvm : user_verification_methods) {
Vector<uint32_t> uvmEntry = {uvm->user_verification_method,
uvm->key_protection_type,
uvm->matcher_protection_type};
uvm_array.push_back(uvmEntry);
}
return uvm_array;
}
#endif
void OnMakePublicKeyCredentialComplete(
std::unique_ptr<ScopedPromiseResolver> scoped_resolver,
AuthenticatorStatus status,
MakeCredentialAuthenticatorResponsePtr 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->info->client_data_json.IsEmpty());
DCHECK(!credential->attestation_object.IsEmpty());
UseCounter::Count(
resolver->GetExecutionContext(),
WebFeature::kCredentialManagerMakePublicKeyCredentialSuccess);
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));
auto* authenticator_response =
MakeGarbageCollected<AuthenticatorAttestationResponse>(
client_data_buffer, attestation_buffer, credential->transports);
AuthenticationExtensionsClientOutputs* extension_outputs =
AuthenticationExtensionsClientOutputs::Create();
if (credential->echo_hmac_create_secret) {
extension_outputs->setHmacCreateSecret(credential->hmac_create_secret);
}
resolver->Resolve(MakeGarbageCollected<PublicKeyCredential>(
credential->info->id, raw_id, authenticator_response,
extension_outputs));
} 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());
UseCounter::Count(
resolver->GetExecutionContext(),
WebFeature::kCredentialManagerGetPublicKeyCredentialSuccess);
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 && credential->user_handle->size() > 0)
? VectorToDOMArrayBuffer(std::move(*credential->user_handle))
: nullptr;
auto* authenticator_response =
MakeGarbageCollected<AuthenticatorAssertionResponse>(
client_data_buffer, authenticator_buffer, signature_buffer,
user_handle);
AuthenticationExtensionsClientOutputs* extension_outputs =
AuthenticationExtensionsClientOutputs::Create();
if (credential->echo_appid_extension) {
extension_outputs->setAppid(credential->appid_extension);
}
#if defined(OS_ANDROID)
if (credential->echo_user_verification_methods) {
extension_outputs->setUvm(
UvmEntryToArray(std::move(*credential->user_verification_methods)));
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kCredentialManagerGetSuccessWithUVM);
}
#endif
resolver->Resolve(MakeGarbageCollected<PublicKeyCredential>(
credential->info->id, raw_id, authenticator_response,
extension_outputs));
} else {
DCHECK(!credential);
resolver->Reject(CredentialManagerErrorToDOMException(
mojo::ConvertTo<CredentialManagerError>(status)));
}
}
void OnSmsReceive(ScriptPromiseResolver* resolver,
base::TimeTicks start_time,
mojom::blink::SmsStatus status,
const WTF::String& otp,
const WTF::String& sms) {
AssertSecurityRequirementsBeforeResponse(
resolver, RequiredOriginType::kSecureAndSameWithAncestors);
auto& document =
Document::From(*ExecutionContext::From(resolver->GetScriptState()));
ukm::SourceId source_id = document.UkmSourceID();
ukm::UkmRecorder* recorder = document.UkmRecorder();
if (status == mojom::blink::SmsStatus::kTimeout) {
RecordSmsOutcome(SMSReceiverOutcome::kTimeout, source_id, recorder);
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kTimeoutError, "OTP retrieval timed out."));
return;
} else if (status == mojom::blink::SmsStatus::kAborted) {
RecordSmsOutcome(SMSReceiverOutcome::kAborted, source_id, recorder);
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kAbortError, "OTP retrieval was aborted."));
return;
} else if (status == mojom::blink::SmsStatus::kCancelled) {
RecordSmsOutcome(SMSReceiverOutcome::kCancelled, source_id, recorder);
RecordSmsCancelTime(base::TimeTicks::Now() - start_time);
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kAbortError, "OTP retrieval was cancelled."));
return;
}
RecordSmsSuccessTime(base::TimeTicks::Now() - start_time);
RecordSmsOutcome(SMSReceiverOutcome::kSuccess, source_id, recorder);
resolver->Resolve(MakeGarbageCollected<OtpCredential>(otp));
}
} // namespace
CredentialsContainer::CredentialsContainer() = default;
ScriptPromise CredentialsContainer::get(
ScriptState* script_state,
const CredentialRequestOptions* options) {
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise promise = resolver->Promise();
// hasPublicKey() implies that this is a WebAuthn request.
auto required_origin_type =
options->hasPublicKey() &&
RuntimeEnabledFeatures::WebAuthenticationFeaturePolicyEnabled()
? RequiredOriginType::kSecureAndPermittedByFeaturePolicy
: RequiredOriginType::kSecureAndSameWithAncestors;
if (!CheckSecurityRequirementsBeforeRequest(resolver, required_origin_type)) {
return promise;
}
if (options->hasPublicKey()) {
auto cryptotoken_origin = SecurityOrigin::Create(KURL(kCryptotokenOrigin));
if (cryptotoken_origin->IsSameOriginWith(
resolver->GetFrame()->GetSecurityContext()->GetSecurityOrigin())) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kU2FCryptotokenSign);
} else {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kCredentialManagerGetPublicKeyCredential);
}
#if defined(OS_ANDROID)
if (options->publicKey()->hasExtensions() &&
options->publicKey()->extensions()->hasUvm()) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kCredentialManagerGetWithUVM);
}
#endif
if (options->publicKey()->hasExtensions()) {
if (options->publicKey()->extensions()->hasAppid()) {
const auto& appid = options->publicKey()->extensions()->appid();
if (!appid.IsEmpty()) {
KURL appid_url(appid);
if (!appid_url.IsValid()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSyntaxError,
"The `appid` extension value is neither "
"empty/null nor a valid URL"));
return promise;
}
}
}
if (options->publicKey()->extensions()->hasCableRegistration()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"The 'cableRegistration' extension is only valid when creating "
"a credential"));
return promise;
}
}
if (!options->publicKey()->hasUserVerification()) {
resolver->GetFrame()->Console().AddMessage(MakeGarbageCollected<
ConsoleMessage>(
mojom::ConsoleMessageSource::kJavaScript,
mojom::ConsoleMessageLevel::kWarning,
"publicKey.userVerification was not set to any value in Web "
"Authentication navigator.credentials.get() call. This defaults to "
"'preferred', which is probably not what you want. If in doubt, set "
"to 'discouraged'. See "
"https://chromium.googlesource.com/chromium/src/+/master/content/"
"browser/webauth/uv_preferred.md for details."));
}
if (options->hasSignal()) {
if (options->signal()->aborted()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kAbortError, "Request has been aborted."));
return promise;
}
options->signal()->AddAlgorithm(
WTF::Bind(&AbortPublicKeyRequest, WrapPersistent(script_state)));
}
auto mojo_options =
MojoPublicKeyCredentialRequestOptions::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->GetAssertion(
std::move(mojo_options),
WTF::Bind(
&OnGetAssertionComplete,
WTF::Passed(std::make_unique<ScopedPromiseResolver>(resolver))));
} else {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Required parameters missing in 'options.publicKey'."));
}
return promise;
}
if (options->hasOtp() && options->otp()->hasTransport()) {
if (!options->otp()->transport().Contains("sms")) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Unsupported transport type for OTP Credentials"));
return promise;
}
if (options->hasSignal()) {
if (options->signal()->aborted()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kAbortError, "Request has been aborted."));
return promise;
}
options->signal()->AddAlgorithm(
WTF::Bind(&AbortOtpRequest, WrapPersistent(script_state)));
}
if (!CheckSecurityRequirementsBeforeRequest(
resolver, RequiredOriginType::kSecureAndSameWithAncestors)) {
return promise;
}
auto* sms_receiver =
CredentialManagerProxy::From(script_state)->SmsReceiver();
sms_receiver->Receive(WTF::Bind(&OnSmsReceive, WrapPersistent(resolver),
base::TimeTicks::Now()));
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) {
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise promise = resolver->Promise();
if (!(credential->IsFederatedCredential() ||
credential->IsPasswordCredential())) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Store operation not permitted for PublicKey credentials."));
return promise;
}
if (!CheckSecurityRequirementsBeforeRequest(
resolver, RequiredOriginType::kSecureAndSameWithAncestors)) {
return promise;
}
const KURL& url =
credential->IsFederatedCredential()
? static_cast<const FederatedCredential*>(credential)->iconURL()
: static_cast<const PasswordCredential*>(credential)->iconURL();
if (!IsIconURLNullOrSecure(url)) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::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))));
return promise;
}
ScriptPromise CredentialsContainer::create(
ScriptState* script_state,
const CredentialCreationOptions* options,
ExceptionState& exception_state) {
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise promise = resolver->Promise();
// hasPublicKey() implies that this is a WebAuthn request.
auto required_origin_type =
options->hasPublicKey()
? RuntimeEnabledFeatures::WebAuthenticationFeaturePolicyEnabled()
? RequiredOriginType::kSecureAndPermittedByFeaturePolicy
: RequiredOriginType::kSecureAndSameWithAncestors
: RequiredOriginType::kSecure;
if (!CheckSecurityRequirementsBeforeRequest(resolver, required_origin_type)) {
return promise;
}
if ((options->hasPassword() + options->hasFederated() +
options->hasPublicKey()) != 1) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::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());
auto cryptotoken_origin = SecurityOrigin::Create(KURL(kCryptotokenOrigin));
if (cryptotoken_origin->IsSameOriginWith(
resolver->GetFrame()->GetSecurityContext()->GetSecurityOrigin())) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kU2FCryptotokenRegister);
} else {
UseCounter::Count(
resolver->GetExecutionContext(),
WebFeature::kCredentialManagerCreatePublicKeyCredential);
}
if (options->publicKey()->hasExtensions()) {
if (options->publicKey()->extensions()->hasAppid()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"The 'appid' extension is only valid when requesting an assertion "
"for a pre-existing credential that was registered using the "
"legacy FIDO U2F API."));
return promise;
}
if (options->publicKey()->extensions()->hasAppidExclude()) {
const auto& appid_exclude =
options->publicKey()->extensions()->appidExclude();
if (!appid_exclude.IsEmpty()) {
KURL appid_exclude_url(appid_exclude);
if (!appid_exclude_url.IsValid()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSyntaxError,
"The `appidExclude` extension value is neither "
"empty/null nor a valid URL."));
return promise;
}
}
}
if (options->publicKey()->extensions()->hasCableAuthentication()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"The 'cableAuthentication' extension is only valid when requesting "
"an assertion"));
return promise;
}
}
if (options->hasSignal()) {
if (options->signal()->aborted()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kAbortError, "Request has been aborted."));
return promise;
}
options->signal()->AddAlgorithm(
WTF::Bind(&AbortPublicKeyRequest, WrapPersistent(script_state)));
}
if (options->publicKey()->hasAuthenticatorSelection() &&
!options->publicKey()
->authenticatorSelection()
->hasUserVerification()) {
resolver->GetFrame()->Console().AddMessage(MakeGarbageCollected<
ConsoleMessage>(
mojom::ConsoleMessageSource::kJavaScript,
mojom::ConsoleMessageLevel::kWarning,
"publicKey.authenticatorSelection.userVerification was not set to "
"any value in Web Authentication navigator.credentials.create() "
"call. This defaults to 'preferred', which is probably not what you "
"want. If in doubt, set to 'discouraged'. See "
"https://chromium.googlesource.com/chromium/src/+/master/content/"
"browser/webauth/uv_preferred.md for details"));
}
auto mojo_options =
MojoPublicKeyCredentialCreationOptions::From(options->publicKey());
if (!mojo_options) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Required parameters missing in `options.publicKey`."));
} else if (mojo_options->user->id.size() > 64) {
// https://www.w3.org/TR/webauthn/#user-handle
v8::Isolate* isolate = resolver->GetScriptState()->GetIsolate();
resolver->Reject(V8ThrowException::CreateTypeError(
isolate, "User handle exceeds 64 bytes."));
} else {
if (!mojo_options->relying_party->id) {
mojo_options->relying_party->id = resolver->GetFrame()
->GetSecurityContext()
->GetSecurityOrigin()
->Domain();
}
if (mojo_options->relying_party->icon) {
if (!IsIconURLNullOrSecure(mojo_options->relying_party->icon.value())) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError,
"'rp.icon' should be a secure URL"));
return promise;
}
}
if (mojo_options->user->icon) {
if (!IsIconURLNullOrSecure(mojo_options->user->icon.value())) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError,
"'user.icon' should be a secure URL"));
return promise;
}
}
auto* authenticator =
CredentialManagerProxy::From(script_state)->Authenticator();
authenticator->MakeCredential(
std::move(mojo_options),
WTF::Bind(
&OnMakePublicKeyCredentialComplete,
WTF::Passed(std::make_unique<ScopedPromiseResolver>(resolver))));
}
}
return promise;
}
ScriptPromise CredentialsContainer::preventSilentAccess(
ScriptState* script_state) {
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(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