blob: c109359d5eecafbc944b6fd3488b1f01bcade240 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// 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/credentialmanagement/authentication_credentials_container.h"
#include <memory>
#include <optional>
#include <utility>
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "build/build_config.h"
#include "mojo/public/mojom/base/values.mojom-blink.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/sms/webotp_constants.h"
#include "third_party/blink/public/mojom/credentialmanagement/credential_manager.mojom-blink.h"
#include "third_party/blink/public/mojom/credentialmanagement/credential_type_flags.mojom-blink.h"
#include "third_party/blink/public/mojom/payments/secure_payment_confirmation_service.mojom-blink.h"
#include "third_party/blink/public/mojom/sms/webotp_service.mojom-blink.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/public/platform/web_v8_value_converter.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/core/v8/v8_union_arraybuffer_arraybufferview.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_all_accepted_credentials_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_client_inputs.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_client_outputs.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_large_blob_inputs.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_large_blob_outputs.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_payment_inputs.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_prf_inputs.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_prf_outputs.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_prf_values.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_supplemental_pub_keys_inputs.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_supplemental_pub_keys_outputs.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_properties_output.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_credential_request_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_current_user_details_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_identity_credential_request_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_identity_provider_config.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_identity_provider_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_descriptor.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_request_options.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/bindings/modules/v8/v8_union_htmlformelement_passwordcredentialdata.h"
#include "third_party/blink/renderer/core/dom/abort_signal.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/dom/scoped_abort_state.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/csp/content_security_policy.h"
#include "third_party/blink/renderer/core/frame/frame.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/navigator.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/core/typed_arrays/dom_array_piece.h"
#include "third_party/blink/renderer/modules/credentialmanagement/authenticator_assertion_response.h"
#include "third_party/blink/renderer/modules/credentialmanagement/authenticator_attestation_response.h"
#include "third_party/blink/renderer/modules/credentialmanagement/credential.h"
#include "third_party/blink/renderer/modules/credentialmanagement/credential_manager_proxy.h"
#include "third_party/blink/renderer/modules/credentialmanagement/credential_manager_type_converters.h" // IWYU pragma: keep
#include "third_party/blink/renderer/modules/credentialmanagement/credential_metrics.h"
#include "third_party/blink/renderer/modules/credentialmanagement/credential_utils.h"
#include "third_party/blink/renderer/modules/credentialmanagement/digital_identity_credential.h"
#include "third_party/blink/renderer/modules/credentialmanagement/federated_credential.h"
#include "third_party/blink/renderer/modules/credentialmanagement/identity_credential.h"
#include "third_party/blink/renderer/modules/credentialmanagement/identity_credential_error.h"
#include "third_party/blink/renderer/modules/credentialmanagement/otp_credential.h"
#include "third_party/blink/renderer/modules/credentialmanagement/password_credential.h"
#include "third_party/blink/renderer/modules/credentialmanagement/public_key_credential.h"
#include "third_party/blink/renderer/modules/credentialmanagement/scoped_promise_resolver.h"
#include "third_party/blink/renderer/platform/bindings/exception_code.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "third_party/blink/renderer/platform/wtf/text/base64.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
#include "third_party/blink/renderer/platform/wtf/wtf_size_t.h"
namespace blink {
namespace {
using mojom::blink::AttestationConveyancePreference;
using mojom::blink::AuthenticationExtensionsClientOutputsPtr;
using mojom::blink::AuthenticatorAttachment;
using mojom::blink::AuthenticatorStatus;
using mojom::blink::CredentialInfo;
using mojom::blink::CredentialInfoPtr;
using mojom::blink::CredentialManagerError;
using mojom::blink::CredentialMediationRequirement;
using mojom::blink::WebAuthnDOMExceptionDetailsPtr;
using MojoPublicKeyCredentialCreationOptions =
mojom::blink::PublicKeyCredentialCreationOptions;
using mojom::blink::GetCredentialOptions;
using mojom::blink::MakeCredentialAuthenticatorResponsePtr;
using MojoPublicKeyCredentialRequestOptions =
mojom::blink::PublicKeyCredentialRequestOptions;
using mojom::blink::GetAssertionAuthenticatorResponsePtr;
using mojom::blink::Mediation;
using mojom::blink::RequestTokenStatus;
using payments::mojom::blink::PaymentCredentialStorageStatus;
constexpr size_t kMaxLargeBlobSize = 2048; // 2kb.
// 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-get" permissions
// policy must be enabled. By default "publickey-credentials-get" 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 permissions policies can be
// expressed in various ways, e.g.: |allow| iframe attribute and/or
// permissions-policy header, and may be inherited from parent browsing
// contexts. See Permissions Policy spec.
kSecureAndPermittedByWebAuthGetAssertionPermissionsPolicy,
// Must be a secure origin and the "publickey-credentials-create" permissions
// policy must be enabled. By default "publickey-credentials-create" 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 permissions policies can be
// expressed in various ways, e.g.: |allow| iframe attribute and/or
// permissions-policy header, and may be inherited from parent browsing
// contexts. See Permissions Policy spec.
kSecureAndPermittedByWebAuthCreateCredentialPermissionsPolicy,
// Similar to the enum above, checks the "otp-credentials" permissions policy.
kSecureAndPermittedByWebOTPAssertionPermissionsPolicy,
// Similar to the enum above, checks the "identity-credentials-get"
// permissions policy.
kSecureAndPermittedByFederatedPermissionsPolicy,
// Must be a secure origin with either the "payment" or
// "publickey-credentials-create" permission policy.
kSecureWithPaymentOrCreateCredentialPermissionPolicy,
};
// Returns whether the number of unique origins in the ancestor chain, including
// the current origin are less or equal to |max_unique_origins|.
//
// Examples:
// A.com = 1 unique origin
// A.com -> A.com = 1 unique origin
// A.com -> A.com -> B.com = 2 unique origins
// A.com -> B.com -> B.com = 2 unique origins
// A.com -> B.com -> A.com = 3 unique origins
bool AreUniqueOriginsLessOrEqualTo(const Frame* frame, int max_unique_origins) {
const SecurityOrigin* current_origin =
frame->GetSecurityContext()->GetSecurityOrigin();
int num_unique_origins = 1;
const Frame* parent = frame->Tree().Parent();
while (parent) {
auto* parent_origin = parent->GetSecurityContext()->GetSecurityOrigin();
if (!parent_origin->IsSameOriginWith(current_origin)) {
++num_unique_origins;
current_origin = parent_origin;
}
if (num_unique_origins > max_unique_origins) {
return false;
}
parent = parent->Tree().Parent();
}
return true;
}
const SecurityOrigin* GetSecurityOrigin(const Frame* frame) {
const SecurityContext* frame_security_context = frame->GetSecurityContext();
if (!frame_security_context) {
return nullptr;
}
return frame_security_context->GetSecurityOrigin();
}
bool IsSameSecurityOriginWithAncestors(const Frame* frame) {
const Frame* current = frame;
const SecurityOrigin* frame_origin = GetSecurityOrigin(frame);
if (!frame_origin) {
return false;
}
while (current->Tree().Parent()) {
current = current->Tree().Parent();
const SecurityOrigin* current_security_origin = GetSecurityOrigin(current);
if (!current_security_origin ||
!frame_origin->IsSameOriginWith(current_security_origin)) {
return false;
}
}
return true;
}
bool IsAncestorChainValidForWebOTP(const Frame* frame) {
return AreUniqueOriginsLessOrEqualTo(
frame, kMaxUniqueOriginInAncestorChainForWebOTP);
}
bool CheckSecurityRequirementsBeforeRequest(
ScriptPromiseResolverBase* resolver,
RequiredOriginType required_origin_type) {
if (!CheckGenericSecurityRequirementsForCredentialsContainerRequest(
resolver)) {
return false;
}
switch (required_origin_type) {
case RequiredOriginType::kSecure:
// This has already been checked.
break;
case RequiredOriginType::kSecureAndSameWithAncestors:
if (!IsSameSecurityOriginWithAncestors(
To<LocalDOMWindow>(resolver->GetExecutionContext())
->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', storage of "
"'PublicKeyCredential'."));
return false;
}
break;
case RequiredOriginType::
kSecureAndPermittedByWebAuthGetAssertionPermissionsPolicy:
// The 'publickey-credentials-get' feature's "default allowlist" is
// "self", which means the webauthn feature is allowed by default in
// same-origin child browsing contexts.
if (!resolver->GetExecutionContext()->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::
kPublicKeyCredentialsGet)) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"The 'publickey-credentials-get' feature is not enabled in this "
"document. Permissions Policy may be used to delegate Web "
"Authentication capabilities to cross-origin child frames."));
return false;
} else if (!IsSameSecurityOriginWithAncestors(
To<LocalDOMWindow>(resolver->GetExecutionContext())
->GetFrame())) {
UseCounter::Count(
resolver->GetExecutionContext(),
WebFeature::kCredentialManagerCrossOriginPublicKeyGetRequest);
}
break;
case RequiredOriginType::
kSecureAndPermittedByWebAuthCreateCredentialPermissionsPolicy:
// The 'publickey-credentials-create' feature's "default allowlist" is
// "self", which means the webauthn feature is allowed by default in
// same-origin child browsing contexts.
if (!resolver->GetExecutionContext()->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::
kPublicKeyCredentialsCreate)) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"The 'publickey-credentials-create' feature is not enabled in this "
"document. Permissions Policy may be used to delegate Web "
"Authentication capabilities to cross-origin child frames."));
return false;
} else if (!IsSameSecurityOriginWithAncestors(
To<LocalDOMWindow>(resolver->GetExecutionContext())
->GetFrame())) {
UseCounter::Count(
resolver->GetExecutionContext(),
WebFeature::kCredentialManagerCrossOriginPublicKeyCreateRequest);
}
break;
case RequiredOriginType::
kSecureAndPermittedByWebOTPAssertionPermissionsPolicy:
if (!resolver->GetExecutionContext()->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::kOTPCredentials)) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"The 'otp-credentials' feature is not enabled in this document."));
return false;
}
if (!IsAncestorChainValidForWebOTP(
To<LocalDOMWindow>(resolver->GetExecutionContext())
->GetFrame())) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"More than two unique origins are detected in the origin chain."));
return false;
}
break;
case RequiredOriginType::kSecureAndPermittedByFederatedPermissionsPolicy:
if (!resolver->GetExecutionContext()->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::
kIdentityCredentialsGet)) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"The 'identity-credentials-get' feature is not enabled in this "
"document."));
return false;
}
break;
case RequiredOriginType::
kSecureWithPaymentOrCreateCredentialPermissionPolicy:
// For backwards compatibility, SPC credentials (that is, credentials with
// the "payment" extension set) can be created in a cross-origin iframe
// with either the 'payment' or 'publickey-credentials-create' permission
// set.
//
// Note that SPC only goes through the credentials API for creation and
// not authentication. Authentication flows via the Payment Request API,
// which checks for the 'payment' permission separately.
if (!resolver->GetExecutionContext()->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::kPayment) &&
!resolver->GetExecutionContext()->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::
kPublicKeyCredentialsCreate)) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"The 'payment' or 'publickey-credentials-create' features are not "
"enabled in this document. Permissions Policy may be used to "
"delegate Web Payment capabilities to cross-origin child frames."));
return false;
}
break;
}
return true;
}
void AssertSecurityRequirementsBeforeResponse(
ScriptPromiseResolverBase* 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(To<LocalDOMWindow>(resolver->GetExecutionContext()));
SECURITY_CHECK(resolver->GetExecutionContext()->IsSecureContext());
switch (require_origin) {
case RequiredOriginType::kSecure:
// This has already been checked.
break;
case RequiredOriginType::kSecureAndSameWithAncestors:
SECURITY_CHECK(IsSameSecurityOriginWithAncestors(
To<LocalDOMWindow>(resolver->GetExecutionContext())->GetFrame()));
break;
case RequiredOriginType::
kSecureAndPermittedByWebAuthGetAssertionPermissionsPolicy:
SECURITY_CHECK(resolver->GetExecutionContext()->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::kPublicKeyCredentialsGet));
break;
case RequiredOriginType::
kSecureAndPermittedByWebAuthCreateCredentialPermissionsPolicy:
SECURITY_CHECK(resolver->GetExecutionContext()->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::
kPublicKeyCredentialsCreate));
break;
case RequiredOriginType::
kSecureAndPermittedByWebOTPAssertionPermissionsPolicy:
SECURITY_CHECK(
resolver->GetExecutionContext()->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::kOTPCredentials) &&
IsAncestorChainValidForWebOTP(
To<LocalDOMWindow>(resolver->GetExecutionContext())->GetFrame()));
break;
case RequiredOriginType::kSecureAndPermittedByFederatedPermissionsPolicy:
SECURITY_CHECK(resolver->GetExecutionContext()->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::kIdentityCredentialsGet));
break;
case RequiredOriginType::
kSecureWithPaymentOrCreateCredentialPermissionPolicy:
SECURITY_CHECK(resolver->GetExecutionContext()->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::kPayment) ||
resolver->GetExecutionContext()->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::
kPublicKeyCredentialsCreate));
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;
}
return network::IsUrlPotentiallyTrustworthy(GURL(url));
}
// Checks if the size of the supplied ArrayBuffer or ArrayBufferView is at most
// the maximum size allowed.
bool IsArrayBufferOrViewBelowSizeLimit(
const V8UnionArrayBufferOrArrayBufferView* buffer_or_view) {
if (!buffer_or_view) {
return true;
}
return base::CheckedNumeric<wtf_size_t>(
DOMArrayPiece(buffer_or_view).ByteLength())
.IsValid();
}
bool IsCredentialDescriptorListBelowSizeLimit(
const HeapVector<Member<PublicKeyCredentialDescriptor>>& list) {
return list.size() <= mojom::blink::kPublicKeyCredentialDescriptorListMaxSize;
}
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::UNKNOWN:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotReadableError,
"An unknown error occurred while talking "
"to the credential manager.");
case CredentialManagerError::SUCCESS:
NOTREACHED();
}
return nullptr;
}
// Abort an ongoing IdentityCredential request. This will only be called before
// the request finishes due to `scoped_abort_state`.
void AbortIdentityCredentialRequest(ScriptState* script_state) {
if (!script_state->ContextIsValid()) {
return;
}
auto* auth_request =
CredentialManagerProxy::From(script_state)->FederatedAuthRequest();
auth_request->CancelTokenRequest();
}
void OnRequestToken(std::unique_ptr<ScopedPromiseResolver> scoped_resolver,
std::unique_ptr<ScopedAbortState> scoped_abort_state,
const CredentialRequestOptions* options,
RequestTokenStatus status,
const std::optional<KURL>& selected_idp_config_url,
std::optional<base::Value> token_value,
mojom::blink::TokenErrorPtr error,
bool is_auto_selected) {
auto* resolver =
scoped_resolver->Release()->DowncastTo<IDLNullable<Credential>>();
switch (status) {
case RequestTokenStatus::kErrorTooManyRequests: {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"Only one navigator.credentials.get request may be outstanding at "
"one time."));
return;
}
case RequestTokenStatus::kErrorCanceled: {
AbortSignal* signal =
scoped_abort_state ? scoped_abort_state->Signal() : nullptr;
if (signal && signal->aborted()) {
auto* script_state = resolver->GetScriptState();
ScriptState::Scope script_state_scope(script_state);
resolver->Reject(signal->reason(script_state));
} else {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kAbortError, "The request has been aborted."));
}
return;
}
case RequestTokenStatus::kError: {
if (!error) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNetworkError, "Error retrieving a token."));
return;
}
resolver->Reject(MakeGarbageCollected<IdentityCredentialError>(
"Error retrieving a token.", error->code, error->url));
return;
}
case RequestTokenStatus::kSuccess: {
CHECK(selected_idp_config_url);
CHECK(token_value);
auto* script_state = resolver->GetScriptState();
ScriptState::Scope script_state_scope(script_state);
ScriptValue token_script_value;
// Create WebV8ValueConverter and convert base::Value to v8::Value
auto converter = Platform::Current()->CreateWebV8ValueConverter();
v8::Local<v8::Value> v8_value =
converter->ToV8Value(*token_value, script_state->GetContext());
token_script_value = ScriptValue(script_state->GetIsolate(), v8_value);
IdentityCredential* credential = IdentityCredential::Create(
token_script_value, is_auto_selected, *selected_idp_config_url);
resolver->Resolve(credential);
return;
}
default: {
NOTREACHED();
}
}
}
void OnStoreComplete(std::unique_ptr<ScopedPromiseResolver> scoped_resolver) {
auto* resolver = scoped_resolver->Release()->DowncastTo<Credential>();
AssertSecurityRequirementsBeforeResponse(
resolver, RequiredOriginType::kSecureAndSameWithAncestors);
resolver->Resolve();
}
void OnPreventSilentAccessComplete(
std::unique_ptr<ScopedPromiseResolver> scoped_resolver) {
auto* resolver = scoped_resolver->Release()->DowncastTo<IDLUndefined>();
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,
Mediation mediation,
CredentialManagerError error,
CredentialInfoPtr credential_info) {
auto* resolver =
scoped_resolver->Release()->DowncastTo<IDLNullable<Credential>>();
AssertSecurityRequirementsBeforeResponse(resolver, required_origin_type);
if (error != CredentialManagerError::SUCCESS) {
DCHECK(!credential_info);
if (mediation == Mediation::IMMEDIATE) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kCredentialsGetImmediateMediationFailure);
}
resolver->Reject(CredentialManagerErrorToDOMException(error));
return;
}
DCHECK(credential_info);
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kCredentialManagerGetReturnedCredential);
if (mediation == Mediation::IMMEDIATE) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kCredentialsGetImmediateMediationPasswordSuccess);
}
resolver->Resolve(mojo::ConvertTo<Credential*>(std::move(credential_info)));
}
DOMArrayBuffer* VectorToDOMArrayBuffer(const Vector<uint8_t> buffer) {
return DOMArrayBuffer::Create(buffer);
}
AuthenticationExtensionsPRFValues* GetPRFExtensionResults(
const mojom::blink::PRFValuesPtr& prf_results) {
auto* values = AuthenticationExtensionsPRFValues::Create();
values->setFirst(MakeGarbageCollected<V8UnionArrayBufferOrArrayBufferView>(
VectorToDOMArrayBuffer(std::move(prf_results->first))));
if (prf_results->second) {
values->setSecond(MakeGarbageCollected<V8UnionArrayBufferOrArrayBufferView>(
VectorToDOMArrayBuffer(std::move(prf_results->second.value()))));
}
return values;
}
void OnMakePublicKeyCredentialComplete(
std::unique_ptr<ScopedPromiseResolver> scoped_resolver,
std::unique_ptr<ScopedAbortState> scoped_abort_state,
FrameOrWorkerScheduler::SchedulingAffectingFeatureHandle feature_handle,
RequiredOriginType required_origin_type,
bool is_rk_required,
AuthenticatorStatus status,
MakeCredentialAuthenticatorResponsePtr credential,
WebAuthnDOMExceptionDetailsPtr dom_exception_details) {
auto* resolver =
scoped_resolver->Release()->DowncastTo<IDLNullable<Credential>>();
AssertSecurityRequirementsBeforeResponse(resolver, required_origin_type);
if (status != AuthenticatorStatus::SUCCESS) {
DCHECK(!credential);
AbortSignal* signal =
scoped_abort_state ? scoped_abort_state->Signal() : nullptr;
if (signal && signal->aborted()) {
auto* script_state = resolver->GetScriptState();
ScriptState::Scope script_state_scope(script_state);
resolver->Reject(signal->reason(script_state));
} else {
resolver->Reject(
AuthenticatorStatusToDOMException(status, dom_exception_details));
}
return;
}
DCHECK(credential);
DCHECK(!credential->info->client_data_json.empty());
DCHECK(!credential->attestation_object.empty());
UseCounter::Count(
resolver->GetExecutionContext(),
WebFeature::kCredentialManagerMakePublicKeyCredentialSuccess);
if (is_rk_required) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kWebAuthnRkRequiredCreationSuccess);
}
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));
DOMArrayBuffer* authenticator_data =
VectorToDOMArrayBuffer(std::move(credential->info->authenticator_data));
DOMArrayBuffer* public_key_der = nullptr;
if (credential->public_key_der) {
public_key_der =
VectorToDOMArrayBuffer(std::move(credential->public_key_der.value()));
}
auto* authenticator_response =
MakeGarbageCollected<AuthenticatorAttestationResponse>(
client_data_buffer, attestation_buffer, credential->transports,
authenticator_data, public_key_der, credential->public_key_algo);
AuthenticationExtensionsClientOutputs* extension_outputs =
AuthenticationExtensionsClientOutputs::Create();
if (credential->echo_hmac_create_secret) {
extension_outputs->setHmacCreateSecret(credential->hmac_create_secret);
}
if (credential->echo_cred_props) {
CredentialPropertiesOutput* cred_props_output =
CredentialPropertiesOutput::Create();
if (credential->has_cred_props_rk) {
cred_props_output->setRk(credential->cred_props_rk);
}
extension_outputs->setCredProps(cred_props_output);
}
if (credential->echo_cred_blob) {
extension_outputs->setCredBlob(credential->cred_blob);
}
if (credential->echo_large_blob) {
AuthenticationExtensionsLargeBlobOutputs* large_blob_outputs =
AuthenticationExtensionsLargeBlobOutputs::Create();
large_blob_outputs->setSupported(credential->supports_large_blob);
extension_outputs->setLargeBlob(large_blob_outputs);
}
if (credential->supplemental_pub_keys) {
extension_outputs->setSupplementalPubKeys(
ConvertTo<AuthenticationExtensionsSupplementalPubKeysOutputs*>(
credential->supplemental_pub_keys));
}
if (credential->payment) {
CHECK(base::FeatureList::IsEnabled(
blink::features::kSecurePaymentConfirmationBrowserBoundKeys));
extension_outputs->setPayment(
ConvertTo<blink::AuthenticationExtensionsPaymentOutputs*>(
credential->payment));
}
if (credential->echo_prf) {
auto* prf_outputs = AuthenticationExtensionsPRFOutputs::Create();
prf_outputs->setEnabled(credential->prf);
if (credential->prf_results) {
prf_outputs->setResults(GetPRFExtensionResults(credential->prf_results));
}
extension_outputs->setPrf(prf_outputs);
}
resolver->Resolve(MakeGarbageCollected<PublicKeyCredential>(
credential->info->id, raw_id, authenticator_response,
credential->authenticator_attachment, extension_outputs));
}
bool IsForPayment(const CredentialCreationOptions* options,
ExecutionContext* context) {
return RuntimeEnabledFeatures::SecurePaymentConfirmationEnabled(context) &&
options->hasPublicKey() && options->publicKey()->hasExtensions() &&
options->publicKey()->extensions()->hasPayment() &&
options->publicKey()->extensions()->payment()->hasIsPayment() &&
options->publicKey()->extensions()->payment()->isPayment();
}
void OnSaveCredentialIdForPaymentExtension(
std::unique_ptr<ScopedPromiseResolver> scoped_resolver,
std::unique_ptr<ScopedAbortState> scoped_abort_state,
FrameOrWorkerScheduler::SchedulingAffectingFeatureHandle feature_handle,
MakeCredentialAuthenticatorResponsePtr credential,
PaymentCredentialStorageStatus storage_status) {
auto status = AuthenticatorStatus::SUCCESS;
if (storage_status != PaymentCredentialStorageStatus::SUCCESS) {
status =
AuthenticatorStatus::FAILED_TO_SAVE_CREDENTIAL_ID_FOR_PAYMENT_EXTENSION;
credential = nullptr;
}
OnMakePublicKeyCredentialComplete(
std::move(scoped_resolver), std::move(scoped_abort_state),
std::move(feature_handle),
RequiredOriginType::kSecureWithPaymentOrCreateCredentialPermissionPolicy,
/*is_rk_required=*/false, status, std::move(credential),
/*dom_exception_details=*/nullptr);
}
void OnMakePublicKeyCredentialWithPaymentExtensionComplete(
std::unique_ptr<ScopedPromiseResolver> scoped_resolver,
std::unique_ptr<ScopedAbortState> scoped_abort_state,
FrameOrWorkerScheduler::SchedulingAffectingFeatureHandle feature_handle,
const String& rp_id_for_payment_extension,
const Vector<uint8_t>& user_id_for_payment_extension,
AuthenticatorStatus status,
MakeCredentialAuthenticatorResponsePtr credential,
WebAuthnDOMExceptionDetailsPtr dom_exception_details) {
auto* resolver =
scoped_resolver->Release()->DowncastTo<IDLNullable<Credential>>();
AssertSecurityRequirementsBeforeResponse(
resolver,
RequiredOriginType::kSecureWithPaymentOrCreateCredentialPermissionPolicy);
if (status != AuthenticatorStatus::SUCCESS) {
DCHECK(!credential);
AbortSignal* signal =
scoped_abort_state ? scoped_abort_state->Signal() : nullptr;
if (signal && signal->aborted()) {
auto* script_state = resolver->GetScriptState();
ScriptState::Scope script_state_scope(script_state);
resolver->Reject(signal->reason(script_state));
} else {
resolver->Reject(
AuthenticatorStatusToDOMException(status, dom_exception_details));
}
return;
}
Vector<uint8_t> credential_id = credential->info->raw_id;
auto* spc_service = CredentialManagerProxy::From(resolver->GetScriptState())
->SecurePaymentConfirmationService();
spc_service->StorePaymentCredential(
std::move(credential_id), rp_id_for_payment_extension,
std::move(user_id_for_payment_extension),
BindOnce(&OnSaveCredentialIdForPaymentExtension,
std::make_unique<ScopedPromiseResolver>(resolver),
std::move(scoped_abort_state), std::move(feature_handle),
std::move(credential)));
}
void OnGetAssertionComplete(
std::unique_ptr<ScopedPromiseResolver> scoped_resolver,
std::unique_ptr<ScopedAbortState> scoped_abort_state,
FrameOrWorkerScheduler::SchedulingAffectingFeatureHandle feature_handle,
Mediation mediation,
AuthenticatorStatus status,
GetAssertionAuthenticatorResponsePtr credential,
WebAuthnDOMExceptionDetailsPtr dom_exception_details) {
auto* resolver =
scoped_resolver->Release()->DowncastTo<IDLNullable<Credential>>();
const auto required_origin_type = RequiredOriginType::kSecure;
AssertSecurityRequirementsBeforeResponse(resolver, required_origin_type);
if (status == AuthenticatorStatus::SUCCESS) {
DCHECK(credential);
DCHECK(!credential->signature.empty());
DCHECK(!credential->info->authenticator_data.empty());
UseCounter::Count(
resolver->GetExecutionContext(),
WebFeature::kCredentialManagerGetPublicKeyCredentialSuccess);
if (mediation == Mediation::CONDITIONAL) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kWebAuthnConditionalUiGetSuccess);
} else if (mediation == Mediation::IMMEDIATE) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kCredentialsGetImmediateMediationPublicKeySuccess);
}
auto* authenticator_response =
MakeGarbageCollected<AuthenticatorAssertionResponse>(
std::move(credential->info->client_data_json),
std::move(credential->info->authenticator_data),
std::move(credential->signature), credential->user_handle);
AuthenticationExtensionsClientOutputs* extension_outputs =
ConvertTo<AuthenticationExtensionsClientOutputs*>(
credential->extensions);
#if BUILDFLAG(IS_ANDROID)
if (credential->extensions->echo_user_verification_methods) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kCredentialManagerGetSuccessWithUVM);
}
#endif
resolver->Resolve(MakeGarbageCollected<PublicKeyCredential>(
credential->info->id,
VectorToDOMArrayBuffer(std::move(credential->info->raw_id)),
authenticator_response, credential->authenticator_attachment,
extension_outputs));
return;
}
if (mediation == Mediation::IMMEDIATE) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kCredentialsGetImmediateMediationFailure);
}
DCHECK(!credential);
AbortSignal* signal =
scoped_abort_state ? scoped_abort_state->Signal() : nullptr;
if (signal && signal->aborted()) {
auto* script_state = resolver->GetScriptState();
ScriptState::Scope script_state_scope(script_state);
resolver->Reject(signal->reason(script_state));
} else {
resolver->Reject(
AuthenticatorStatusToDOMException(status, dom_exception_details));
}
}
void OnAuthenticatorGetCredentialComplete(
std::unique_ptr<ScopedPromiseResolver> scoped_resolver,
std::unique_ptr<ScopedAbortState> scoped_abort_state,
FrameOrWorkerScheduler::SchedulingAffectingFeatureHandle feature_handle,
Mediation mediation,
mojom::blink::GetCredentialResponsePtr get_credential_response) {
if (!get_credential_response) {
return;
}
if (get_credential_response->is_get_assertion_response()) {
auto get_assertion_response =
std::move(get_credential_response->get_get_assertion_response());
OnGetAssertionComplete(
std::move(scoped_resolver), std::move(scoped_abort_state),
std::move(feature_handle), mediation,
std::move(get_assertion_response->status),
std::move(get_assertion_response->credential),
std::move(get_assertion_response->dom_exception_details));
return;
}
auto password_response =
std::move(get_credential_response->get_password_response());
OnGetComplete(std::move(scoped_resolver), RequiredOriginType::kSecure,
mediation, CredentialManagerError::SUCCESS, std::move(password_response));
}
void OnSmsReceive(ScriptPromiseResolver<IDLNullable<Credential>>* resolver,
std::unique_ptr<ScopedAbortState> scoped_abort_state,
base::TimeTicks start_time,
mojom::blink::SmsStatus status,
const String& otp) {
AssertSecurityRequirementsBeforeResponse(
resolver, resolver->GetExecutionContext()->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::kOTPCredentials)
? RequiredOriginType::
kSecureAndPermittedByWebOTPAssertionPermissionsPolicy
: RequiredOriginType::kSecureAndSameWithAncestors);
if (status == mojom::blink::SmsStatus::kUnhandledRequest) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError,
"OTP retrieval request not handled."));
return;
}
if (status == mojom::blink::SmsStatus::kAborted) {
AbortSignal* signal =
scoped_abort_state ? scoped_abort_state->Signal() : nullptr;
if (signal && signal->aborted()) {
auto* script_state = resolver->GetScriptState();
ScriptState::Scope script_state_scope(script_state);
resolver->Reject(signal->reason(script_state));
} else {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kAbortError, "OTP retrieval was aborted."));
}
return;
}
if (status == mojom::blink::SmsStatus::kCancelled) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kAbortError, "OTP retrieval was cancelled."));
return;
}
if (status == mojom::blink::SmsStatus::kTimeout) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError, "OTP retrieval timed out."));
return;
}
if (status == mojom::blink::SmsStatus::kBackendNotAvailable) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError, "OTP backend unavailable."));
return;
}
resolver->Resolve(MakeGarbageCollected<OTPCredential>(otp));
}
// Validates the "payment" extension for public key credential creation. The
// function rejects the promise before returning in this case.
bool IsPaymentExtensionValid(const CredentialCreationOptions* options,
ScriptPromiseResolverBase* resolver) {
const auto* payment = options->publicKey()->extensions()->payment();
if (!payment->hasIsPayment() || !payment->isPayment()) {
return true;
}
const auto* context = resolver->GetExecutionContext();
DCHECK(RuntimeEnabledFeatures::SecurePaymentConfirmationEnabled(context));
if (RuntimeEnabledFeatures::SecurePaymentConfirmationDebugEnabled()) {
return true;
}
if (!options->publicKey()->hasAuthenticatorSelection()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"A user verifying platform authenticator with resident key support is "
"required for 'payment' extension."));
return false;
}
const auto* authenticator = options->publicKey()->authenticatorSelection();
if (!authenticator->hasUserVerification() ||
authenticator->userVerification() != "required") {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"User verification is required for 'payment' extension."));
return false;
}
if ((!authenticator->hasResidentKey() &&
!authenticator->hasRequireResidentKey()) ||
(authenticator->hasResidentKey() &&
authenticator->residentKey() == "discouraged") ||
(!authenticator->hasResidentKey() &&
authenticator->hasRequireResidentKey() &&
!authenticator->requireResidentKey())) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"A resident key must be 'preferred' or 'required' for 'payment' "
"extension."));
return false;
}
if (!authenticator->hasAuthenticatorAttachment() ||
authenticator->authenticatorAttachment() != "platform") {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"A platform authenticator is required for 'payment' extension."));
return false;
}
return true;
}
const char* validatePRFInputs(
const blink::AuthenticationExtensionsPRFValues& values) {
constexpr size_t kMaxInputSize = 256;
if (DOMArrayPiece(values.first()).ByteLength() > kMaxInputSize ||
(values.hasSecond() &&
DOMArrayPiece(values.second()).ByteLength() > kMaxInputSize)) {
return "'prf' extension contains excessively large input";
}
return nullptr;
}
const char* validateCreatePublicKeyCredentialPRFExtension(
const AuthenticationExtensionsPRFInputs& prf) {
if (prf.hasEval()) {
const char* error = validatePRFInputs(*prf.eval());
if (error != nullptr) {
return error;
}
}
if (prf.hasEvalByCredential()) {
return "The 'evalByCredential' field cannot be set when creating a "
"credential.";
}
return nullptr;
}
const char* validateGetPublicKeyCredentialPRFExtension(
const AuthenticationExtensionsPRFInputs& prf,
const HeapVector<Member<PublicKeyCredentialDescriptor>>&
allow_credentials) {
std::vector<base::span<const uint8_t>> cred_ids;
cred_ids.reserve(allow_credentials.size());
for (const auto cred : allow_credentials) {
DOMArrayPiece piece(cred->id());
cred_ids.emplace_back(piece.Bytes(), piece.ByteLength());
}
const auto compare = [](base::span<const uint8_t> a,
base::span<const uint8_t> b) {
return std::ranges::lexicographical_compare(a, b);
};
std::ranges::sort(cred_ids, compare);
if (prf.hasEval()) {
const char* error = validatePRFInputs(*prf.eval());
if (error != nullptr) {
return error;
}
}
if (prf.hasEvalByCredential()) {
for (const auto& pair : prf.evalByCredential()) {
Vector<uint8_t> cred_id;
if (!pair.first.Is8Bit() ||
!Base64UnpaddedURLDecode(pair.first, cred_id)) {
return "'prf' extension contains invalid base64url data in "
"'evalByCredential'";
}
if (cred_id.empty()) {
return "'prf' extension contains an empty credential ID in "
"'evalByCredential'";
}
if (!std::ranges::binary_search(cred_ids, base::as_byte_span(cred_id),
compare)) {
return "'prf' extension contains 'evalByCredential' key that doesn't "
"match any in allowedCredentials";
}
const char* error = validatePRFInputs(*pair.second);
if (error != nullptr) {
return error;
}
}
}
return nullptr;
}
void EmitImmediateMediationUseCounters(
ExecutionContext* context,
const CredentialRequestOptions* options) {
CHECK(options->hasMediation() &&
options->mediation() ==
V8CredentialMediationRequirement::Enum::kImmediate);
if (options->hasPublicKey() && options->password()) {
UseCounter::Count(
context,
WebFeature::kCredentialsGetImmediateMediationWithWebAuthnAndPasswords);
} else if (options->hasPublicKey()) {
UseCounter::Count(
context, WebFeature::kCredentialsGetImmediateMediationWithWebAuthnOnly);
} else if (options->password()) {
UseCounter::Count(
context,
WebFeature::kCredentialsGetImmediateMediationWithPasswordsOnly);
}
}
} // namespace
const char AuthenticationCredentialsContainer::kSupplementName[] =
"AuthenticationCredentialsContainer";
DOMException* AuthenticatorStatusToDOMException(
AuthenticatorStatus status,
const WebAuthnDOMExceptionDetailsPtr& dom_exception_details) {
DCHECK_EQ(status != AuthenticatorStatus::ERROR_WITH_DOM_EXCEPTION_DETAILS,
dom_exception_details.is_null());
switch (status) {
case AuthenticatorStatus::SUCCESS:
NOTREACHED();
case AuthenticatorStatus::PENDING_REQUEST:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kOperationError, "A request is already pending.");
case AuthenticatorStatus::NOT_ALLOWED_ERROR:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"The operation either timed out or was not allowed. See: "
"https://www.w3.org/TR/webauthn-2/"
"#sctn-privacy-considerations-client.");
case AuthenticatorStatus::INVALID_DOMAIN:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError, "This is an invalid domain.");
case AuthenticatorStatus::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 AuthenticatorStatus::NOT_IMPLEMENTED:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError, "Not implemented");
case AuthenticatorStatus::NOT_FOCUSED:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"The operation is not allowed at this time "
"because the page does not have focus.");
case AuthenticatorStatus::RESIDENT_CREDENTIALS_UNSUPPORTED:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Resident credentials or empty "
"'allowCredentials' lists are not supported "
"at this time.");
case AuthenticatorStatus::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 AuthenticatorStatus::ALGORITHM_UNSUPPORTED:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"None of the algorithms specified in "
"`pubKeyCredParams` are supported by "
"this device.");
case AuthenticatorStatus::EMPTY_ALLOW_CREDENTIALS:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Use of an empty `allowCredentials` list is "
"not supported on this device.");
case AuthenticatorStatus::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 AuthenticatorStatus::PROTECTION_POLICY_INCONSISTENT:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Requested protection policy is inconsistent or incongruent with "
"other requested parameters.");
case AuthenticatorStatus::ABORT_ERROR:
return MakeGarbageCollected<DOMException>(DOMExceptionCode::kAbortError,
"Request has been aborted.");
case AuthenticatorStatus::OPAQUE_DOMAIN:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"The current origin is an opaque origin and hence not allowed to "
"access 'PublicKeyCredential' objects.");
case AuthenticatorStatus::INVALID_PROTOCOL:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError,
"Public-key credentials are only available to HTTPS origins with "
"valid certificates, HTTP origins that fall under 'localhost', or "
"pages served from an extension. See "
"https://chromium.googlesource.com/chromium/src/+/main/content/"
"browser/webauth/origins.md for details");
case AuthenticatorStatus::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 AuthenticatorStatus::BAD_RELYING_PARTY_ID_ATTEMPTED_FETCH:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError,
"The relying party ID is not a registrable domain suffix of, nor "
"equal to the current domain. Subsequently, an attempt to fetch the "
".well-known/webauthn resource of the claimed RP ID failed.");
case AuthenticatorStatus::BAD_RELYING_PARTY_ID_WRONG_CONTENT_TYPE:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError,
"The relying party ID is not a registrable domain suffix of, nor "
"equal to the current domain. Subsequently, the "
".well-known/webauthn resource of the claimed RP ID had the "
"wrong content-type. (It should be application/json.)");
case AuthenticatorStatus::BAD_RELYING_PARTY_ID_JSON_PARSE_ERROR:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError,
"The relying party ID is not a registrable domain suffix of, nor "
"equal to the current domain. Subsequently, fetching the "
".well-known/webauthn resource of the claimed RP ID resulted "
"in a JSON parse error.");
case AuthenticatorStatus::BAD_RELYING_PARTY_ID_NO_JSON_MATCH:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError,
"The relying party ID is not a registrable domain suffix of, nor "
"equal to the current domain. Subsequently, fetching the "
".well-known/webauthn resource of the claimed RP ID was "
"successful, but no listed origin matched the caller.");
case AuthenticatorStatus::BAD_RELYING_PARTY_ID_NO_JSON_MATCH_HIT_LIMITS:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError,
"The relying party ID is not a registrable domain suffix of, nor "
"equal to the current domain. Subsequently, fetching the "
".well-known/webauthn resource of the claimed RP ID was "
"successful, but no listed origin matched the caller. Note that a "
"match may have been found but the limit on the number of eTLD+1 "
"labels was reached, causing some entries to be ignored.");
case AuthenticatorStatus::CANNOT_READ_AND_WRITE_LARGE_BLOB:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Only one of the 'largeBlob' extension's 'read' and 'write' "
"parameters is allowed at a time");
case AuthenticatorStatus::INVALID_ALLOW_CREDENTIALS_FOR_LARGE_BLOB:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"The 'largeBlob' extension's 'write' parameter can only be used "
"with a single credential present on 'allowCredentials'");
case AuthenticatorStatus::
FAILED_TO_SAVE_CREDENTIAL_ID_FOR_PAYMENT_EXTENSION:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotReadableError,
"Failed to save the credential identifier for the 'payment' "
"extension.");
case AuthenticatorStatus::REMOTE_DESKTOP_CLIENT_OVERRIDE_NOT_AUTHORIZED:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"This origin is not permitted to use the "
"'remoteDesktopClientOverride' extension.");
case AuthenticatorStatus::CERTIFICATE_ERROR:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"WebAuthn is not supported on sites with TLS certificate errors.");
case AuthenticatorStatus::ERROR_WITH_DOM_EXCEPTION_DETAILS:
return DOMException::Create(
/*message=*/dom_exception_details->message,
/*name=*/dom_exception_details->name);
case AuthenticatorStatus::DEVICE_PUBLIC_KEY_ATTESTATION_REJECTED:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"The authenticator responded with an invalid message");
case AuthenticatorStatus::UNKNOWN_ERROR:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotReadableError,
"An unknown error occurred while talking "
"to the credential manager.");
case AuthenticatorStatus::IMMEDIATE_NOT_FOUND:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"No immediate discoverable credentials are found.");
}
return nullptr;
}
class AuthenticationCredentialsContainer::OtpRequestAbortAlgorithm final
: public AbortSignal::Algorithm {
public:
explicit OtpRequestAbortAlgorithm(ScriptState* script_state)
: script_state_(script_state) {}
~OtpRequestAbortAlgorithm() override = default;
// Abort an ongoing OtpCredential get() operation.
void Run() override {
if (!script_state_->ContextIsValid()) {
return;
}
auto* webotp_service =
CredentialManagerProxy::From(script_state_)->WebOTPService();
webotp_service->Abort();
}
void Trace(Visitor* visitor) const override {
visitor->Trace(script_state_);
Algorithm::Trace(visitor);
}
private:
Member<ScriptState> script_state_;
};
class AuthenticationCredentialsContainer::PublicKeyRequestAbortAlgorithm final
: public AbortSignal::Algorithm {
public:
explicit PublicKeyRequestAbortAlgorithm(ScriptState* script_state)
: script_state_(script_state) {}
~PublicKeyRequestAbortAlgorithm() override = default;
// Abort an ongoing PublicKeyCredential create() or get() operation.
void Run() override {
if (!script_state_->ContextIsValid()) {
return;
}
auto* authenticator =
CredentialManagerProxy::From(script_state_)->Authenticator();
authenticator->Cancel();
}
void Trace(Visitor* visitor) const override {
visitor->Trace(script_state_);
Algorithm::Trace(visitor);
}
private:
Member<ScriptState> script_state_;
};
CredentialsContainer* AuthenticationCredentialsContainer::credentials(
Navigator& navigator) {
AuthenticationCredentialsContainer* credentials =
Supplement<Navigator>::From<AuthenticationCredentialsContainer>(
navigator);
if (!credentials) {
credentials =
MakeGarbageCollected<AuthenticationCredentialsContainer>(navigator);
ProvideTo(navigator, credentials);
}
return credentials;
}
AuthenticationCredentialsContainer::AuthenticationCredentialsContainer(
Navigator& navigator)
: Supplement<Navigator>(navigator) {}
ScriptPromise<IDLNullable<Credential>> AuthenticationCredentialsContainer::get(
ScriptState* script_state,
const CredentialRequestOptions* options,
ExceptionState& exception_state) {
if (!script_state->ContextIsValid()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
"Context is detached");
return ScriptPromise<IDLNullable<Credential>>();
}
auto* resolver =
MakeGarbageCollected<ScriptPromiseResolver<IDLNullable<Credential>>>(
script_state, exception_state.GetContext());
auto promise = resolver->Promise();
ExecutionContext* context = ExecutionContext::From(script_state);
if (options->hasSignal() && options->signal()->aborted()) {
resolver->Reject(options->signal()->reason(script_state));
return promise;
}
if (RuntimeEnabledFeatures::WebIdentityDigitalCredentialsEnabled(
resolver->GetExecutionContext()) &&
IsDigitalIdentityCredentialType(*options)) {
DiscoverDigitalIdentityCredentialFromExternalSource(resolver, *options);
return promise;
}
if (options->hasPublicKey() && !options->publicKey()->hasChallenge()) {
if (!blink::RuntimeEnabledFeatures::
WebAuthenticationChallengeUrlEnabled()) {
resolver->RejectWithTypeError(
"Failed to read the 'challenge' property from "
"'PublicKeyCredentialRequestOptions'");
return promise;
} else if (!options->publicKey()->hasChallengeUrl()) {
resolver->RejectWithTypeError(
"Failed to read 'challenge' or 'challengeUrl' property from "
"'PublicKeyCredentialRequestOptions'");
return promise;
}
// Relative URLs have to be turned to absolute URLs before the type
// converter builds the mojo struct.
options->publicKey()->setChallengeUrl(
context->CompleteURL(options->publicKey()->challengeUrl()));
}
auto required_origin_type = RequiredOriginType::kSecureAndSameWithAncestors;
// hasPublicKey() implies that this is a WebAuthn request.
if (options->hasPublicKey()) {
required_origin_type = RequiredOriginType::
kSecureAndPermittedByWebAuthGetAssertionPermissionsPolicy;
} else if (options->hasOtp() &&
RuntimeEnabledFeatures::WebOTPAssertionFeaturePolicyEnabled()) {
required_origin_type = RequiredOriginType::
kSecureAndPermittedByWebOTPAssertionPermissionsPolicy;
} else if (options->hasIdentity() && options->identity()->hasProviders() &&
options->identity()->providers().size() == 1) {
required_origin_type =
RequiredOriginType::kSecureAndPermittedByFederatedPermissionsPolicy;
}
if (!CheckSecurityRequirementsBeforeRequest(resolver, required_origin_type)) {
return promise;
}
// TODO(cbiesinger): Consider removing the hasIdentity() check after FedCM
// ships. Before then, it is useful for RPs to pass both identity and
// federated while transitioning from the older to the new API.
if (options->hasFederated() && options->federated()->hasProviders() &&
options->federated()->providers().size() > 0 && !options->hasIdentity()) {
UseCounter::Count(
context, WebFeature::kCredentialManagerGetLegacyFederatedCredential);
}
if (options->hasPassword() && options->password()) {
UseCounter::Count(context,
WebFeature::kCredentialManagerGetPasswordCredential);
}
// TODO(crbug.com/358119268): For prototyping, any conditionally-mediated
// request that contains both password and publicKey credential types is
// assumed to be ambient, when the flag is on. This will change.
if (RuntimeEnabledFeatures::WebAuthenticationAmbientEnabled() &&
options->hasPublicKey() && options->hasPassword() &&
options->password() &&
options->mediation() ==
V8CredentialMediationRequirement::Enum::kConditional) {
// Unsupported ambient credential types:
if (options->hasOtp() || options->hasIdentity() ||
(options->publicKey()->hasExtensions() &&
options->publicKey()->extensions()->hasPayment()) ||
options->hasFederated()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Unsupported combination of credential types requested."));
return promise;
}
}
if (options->hasPublicKey()) {
ForwardRequestToAuthenticator(script_state, resolver, options);
return promise;
}
if (options->hasOtp() && options->otp()->hasTransport()) {
if (!options->otp()->transport().Contains(
V8OTPCredentialTransportType::Enum::kSms)) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Unsupported transport type for OTP Credentials"));
return promise;
}
std::unique_ptr<ScopedAbortState> scoped_abort_state = nullptr;
if (auto* signal = options->getSignalOr(nullptr)) {
auto* handle = signal->AddAlgorithm(
MakeGarbageCollected<OtpRequestAbortAlgorithm>(script_state));
scoped_abort_state = std::make_unique<ScopedAbortState>(signal, handle);
}
auto* webotp_service =
CredentialManagerProxy::From(script_state)->WebOTPService();
webotp_service->Receive(
blink::BindOnce(&OnSmsReceive, WrapPersistent(resolver),
std::move(scoped_abort_state), base::TimeTicks::Now()));
UseCounter::Count(context, WebFeature::kWebOTP);
return promise;
}
if (options->hasIdentity() && options->identity()->hasProviders()) {
GetForIdentity(script_state, resolver, *options, *options->identity());
return promise;
}
Vector<KURL> providers;
if (options->hasFederated() && options->federated()->hasProviders()) {
for (const auto& provider : options->federated()->providers()) {
KURL url = KURL(NullURL(), provider);
if (url.IsValid()) {
providers.push_back(std::move(url));
}
}
}
CredentialMediationRequirement requirement;
if (options->mediation() ==
V8CredentialMediationRequirement::Enum::kConditional) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Conditional mediation is not supported for this credential type"));
return promise;
}
if (options->mediation() ==
V8CredentialMediationRequirement::Enum::kImmediate) {
if (RuntimeEnabledFeatures::WebAuthenticationImmediateGetEnabled(context)) {
if (options->password()) {
if (RuntimeEnabledFeatures::
AuthenticatorPasswordsOnlyImmediateRequestsEnabled(context)) {
ForwardRequestToAuthenticator(script_state, resolver, options);
return promise;
}
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Immediate mediation is not yet implemented for requests that do "
"not accept PublicKeyCredential. An Immediate request for "
"passwords must also include a request for passkeys."));
} else {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Immediate mediation is not supported for this credential type"));
}
} else {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Immediate mediation not implemented"));
}
return promise;
}
switch (options->mediation().AsEnum()) {
case V8CredentialMediationRequirement::Enum::kSilent:
UseCounter::Count(context,
WebFeature::kCredentialManagerGetMediationSilent);
requirement = CredentialMediationRequirement::kSilent;
break;
case V8CredentialMediationRequirement::Enum::kOptional:
UseCounter::Count(context,
WebFeature::kCredentialManagerGetMediationOptional);
requirement = CredentialMediationRequirement::kOptional;
break;
case V8CredentialMediationRequirement::Enum::kRequired:
UseCounter::Count(context,
WebFeature::kCredentialManagerGetMediationRequired);
requirement = CredentialMediationRequirement::kRequired;
break;
case V8CredentialMediationRequirement::Enum::kConditional:
case V8CredentialMediationRequirement::Enum::kImmediate:
NOTREACHED();
}
auto* credential_manager =
CredentialManagerProxy::From(script_state)->CredentialManager();
credential_manager->Get(
requirement, options->password(), std::move(providers),
BindOnce(&OnGetComplete,
std::make_unique<ScopedPromiseResolver>(resolver),
required_origin_type, Mediation::MODAL));
return promise;
}
ScriptPromise<Credential> AuthenticationCredentialsContainer::store(
ScriptState* script_state,
Credential* credential,
ExceptionState& exception_state) {
if (!script_state->ContextIsValid()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
"Context is detached");
return EmptyPromise();
}
auto* resolver =
MakeGarbageCollected<ScriptPromiseResolver<Credential>>(script_state);
auto promise = resolver->Promise();
if (!(credential->IsFederatedCredential() ||
credential->IsPasswordCredential())) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Store operation not permitted for this credential type."));
return promise;
}
if (!CheckSecurityRequirementsBeforeRequest(
resolver, RequiredOriginType::kSecureAndSameWithAncestors)) {
return promise;
}
if (credential->IsFederatedCredential()) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kCredentialManagerStoreFederatedCredential);
} else if (credential->IsPasswordCredential()) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kCredentialManagerStorePasswordCredential);
}
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();
DCHECK_NE(mojom::blink::CredentialType::EMPTY,
CredentialInfo::From(credential)->type);
credential_manager->Store(
CredentialInfo::From(credential),
BindOnce(&OnStoreComplete,
std::make_unique<ScopedPromiseResolver>(resolver)));
return promise;
}
ScriptPromise<IDLNullable<Credential>>
AuthenticationCredentialsContainer::create(
ScriptState* script_state,
const CredentialCreationOptions* options,
ExceptionState& exception_state) {
if (!script_state->ContextIsValid()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
"Context is detached");
return ScriptPromise<IDLNullable<Credential>>();
}
auto* resolver =
MakeGarbageCollected<ScriptPromiseResolver<IDLNullable<Credential>>>(
script_state);
auto promise = resolver->Promise();
if (options->hasSignal() && options->signal()->aborted()) {
resolver->Reject(options->signal()->reason(script_state));
return promise;
}
if (RuntimeEnabledFeatures::WebIdentityDigitalCredentialsCreationEnabled(
resolver->GetExecutionContext()) &&
IsDigitalIdentityCredentialType(*options)) {
CreateDigitalIdentityCredentialInExternalSource(resolver, *options);
return promise;
}
RequiredOriginType required_origin_type;
if (IsForPayment(options, resolver->GetExecutionContext())) {
required_origin_type = RequiredOriginType::
kSecureWithPaymentOrCreateCredentialPermissionPolicy;
} else if (options->hasPublicKey()) {
// hasPublicKey() implies that this is a WebAuthn request.
required_origin_type = RequiredOriginType::
kSecureAndPermittedByWebAuthCreateCredentialPermissionsPolicy;
} else {
required_origin_type = 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()) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kCredentialManagerCreatePasswordCredential);
resolver->Resolve(
options->password()->IsPasswordCredentialData()
? PasswordCredential::Create(
options->password()->GetAsPasswordCredentialData(),
exception_state)
: PasswordCredential::Create(
options->password()->GetAsHTMLFormElement(),
exception_state));
return promise;
}
if (options->hasFederated()) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kCredentialManagerCreateFederatedCredential);
resolver->Resolve(
FederatedCredential::Create(options->federated(), exception_state));
return promise;
}
DCHECK(options->hasPublicKey());
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kCredentialManagerCreatePublicKeyCredential);
if (!IsArrayBufferOrViewBelowSizeLimit(options->publicKey()->challenge())) {
resolver->Reject(DOMException::Create(
"The `challenge` attribute exceeds the maximum allowed size.",
"RangeError"));
return promise;
}
if (!IsArrayBufferOrViewBelowSizeLimit(options->publicKey()->user()->id())) {
resolver->Reject(DOMException::Create(
"The `user.id` attribute exceeds the maximum allowed size.",
"RangeError"));
return promise;
}
if (!IsCredentialDescriptorListBelowSizeLimit(
options->publicKey()->excludeCredentials())) {
resolver->Reject(
DOMException::Create("The `excludeCredentials` attribute exceeds the "
"maximum allowed size (64).",
"RangeError"));
return promise;
}
for (const auto& credential : options->publicKey()->excludeCredentials()) {
if (!IsArrayBufferOrViewBelowSizeLimit(credential->id())) {
resolver->Reject(DOMException::Create(
"The `excludeCredentials.id` attribute exceeds the maximum "
"allowed size.",
"RangeError"));
return promise;
}
}
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.empty()) {
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->publicKey()->extensions()->hasLargeBlob()) {
if (options->publicKey()->extensions()->largeBlob()->hasRead()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"The 'largeBlob' extension's 'read' parameter is only valid when "
"requesting an assertion"));
return promise;
}
if (options->publicKey()->extensions()->largeBlob()->hasWrite()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"The 'largeBlob' extension's 'write' parameter is only valid "
"when requesting an assertion"));
return promise;
}
}
if (options->publicKey()->extensions()->hasPayment() &&
!IsPaymentExtensionValid(options, resolver)) {
return promise;
}
if (options->publicKey()->extensions()->hasPrf()) {
const char* error = validateCreatePublicKeyCredentialPRFExtension(
*options->publicKey()->extensions()->prf());
if (error != nullptr) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError, error));
return promise;
}
}
}
// In the case of create() in a cross-origin iframe, the spec requires that
// the caller must have transient user activation (which is consumed).
// https://w3c.github.io/webauthn/#sctn-createCredential, step 2.
if (!IsSameSecurityOriginWithAncestors(
To<LocalDOMWindow>(resolver->GetExecutionContext())->GetFrame())) {
bool has_user_activation = LocalFrame::ConsumeTransientUserActivation(
To<LocalDOMWindow>(resolver->GetExecutionContext())->GetFrame(),
UserActivationUpdateSource::kRenderer);
if (!has_user_activation) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"A user activation is required to create a credential in a "
"cross-origin iframe."));
return promise;
}
}
std::unique_ptr<ScopedAbortState> scoped_abort_state = nullptr;
if (auto* signal = options->getSignalOr(nullptr)) {
auto* handle = signal->AddAlgorithm(
MakeGarbageCollected<PublicKeyRequestAbortAlgorithm>(script_state));
scoped_abort_state = std::make_unique<ScopedAbortState>(signal, handle);
}
if (options->publicKey()->hasAttestation() &&
!mojo::ConvertTo<std::optional<AttestationConveyancePreference>>(
options->publicKey()->attestation())) {
resolver->GetExecutionContext()->AddConsoleMessage(
MakeGarbageCollected<ConsoleMessage>(
mojom::blink::ConsoleMessageSource::kJavaScript,
mojom::blink::ConsoleMessageLevel::kWarning,
"Ignoring unknown publicKey.attestation value"));
}
if (options->publicKey()->hasAuthenticatorSelection() &&
options->publicKey()
->authenticatorSelection()
->hasAuthenticatorAttachment()) {
std::optional<String> attachment = options->publicKey()
->authenticatorSelection()
->authenticatorAttachment();
if (!mojo::ConvertTo<std::optional<AuthenticatorAttachment>>(attachment)) {
resolver->GetExecutionContext()->AddConsoleMessage(
MakeGarbageCollected<ConsoleMessage>(
mojom::blink::ConsoleMessageSource::kJavaScript,
mojom::blink::ConsoleMessageLevel::kWarning,
"Ignoring unknown "
"publicKey.authenticatorSelection.authnticatorAttachment value"));
}
}
if (options->publicKey()->hasAuthenticatorSelection() &&
options->publicKey()->authenticatorSelection()->hasUserVerification() &&
!mojo::ConvertTo<
std::optional<mojom::blink::UserVerificationRequirement>>(
options->publicKey()->authenticatorSelection()->userVerification())) {
resolver->GetExecutionContext()->AddConsoleMessage(
MakeGarbageCollected<ConsoleMessage>(
mojom::blink::ConsoleMessageSource::kJavaScript,
mojom::blink::ConsoleMessageLevel::kWarning,
"Ignoring unknown "
"publicKey.authenticatorSelection.userVerification value"));
}
bool is_rk_required = false;
if (options->publicKey()->hasAuthenticatorSelection() &&
options->publicKey()->authenticatorSelection()->hasResidentKey()) {
auto rk_requirement =
mojo::ConvertTo<std::optional<mojom::blink::ResidentKeyRequirement>>(
options->publicKey()->authenticatorSelection()->residentKey());
if (!rk_requirement) {
resolver->GetExecutionContext()->AddConsoleMessage(
MakeGarbageCollected<ConsoleMessage>(
mojom::blink::ConsoleMessageSource::kJavaScript,
mojom::blink::ConsoleMessageLevel::kWarning,
"Ignoring unknown publicKey.authenticatorSelection.residentKey "
"value"));
} else {
is_rk_required =
(rk_requirement == mojom::blink::ResidentKeyRequirement::REQUIRED);
}
}
// An empty list uses default algorithm identifiers.
if (options->publicKey()->pubKeyCredParams().size() != 0) {
HashSet<int16_t> algorithm_set;
for (const auto& param : options->publicKey()->pubKeyCredParams()) {
// 0 and -1 are special values that cannot be inserted into the HashSet.
if (param->alg() != 0 && param->alg() != -1) {
algorithm_set.insert(param->alg());
}
}
if (!algorithm_set.Contains(-7) || !algorithm_set.Contains(-257)) {
resolver->GetExecutionContext()->AddConsoleMessage(
MakeGarbageCollected<ConsoleMessage>(
mojom::blink::ConsoleMessageSource::kJavaScript,
mojom::blink::ConsoleMessageLevel::kWarning,
"publicKey.pubKeyCredParams is missing at least one of the "
"default algorithm identifiers: ES256 and RS256. This can "
"result in registration failures on incompatible "
"authenticators. See "
"https://chromium.googlesource.com/chromium/src/+/main/"
"content/browser/webauth/pub_key_cred_params.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`."));
return promise;
}
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."));
return promise;
}
if (!mojo_options->relying_party->id) {
mojo_options->relying_party->id =
resolver->GetExecutionContext()->GetSecurityOrigin()->Domain();
}
auto* authenticator =
CredentialManagerProxy::From(script_state)->Authenticator();
FrameOrWorkerScheduler::SchedulingAffectingFeatureHandle feature_handle =
ExecutionContext::From(script_state)
->GetScheduler()
->RegisterFeature(SchedulingPolicy::Feature::kWebAuthentication,
SchedulingPolicy::DisableBackForwardCache());
if (mojo_options->is_payment_credential_creation) {
String rp_id_for_payment_extension = mojo_options->relying_party->id;
Vector<uint8_t> user_id_for_payment_extension = mojo_options->user->id;
if (base::FeatureList::IsEnabled(
blink::features::kSecurePaymentConfirmationBrowserBoundKeys)) {
auto* spc_service =
CredentialManagerProxy::From(resolver->GetScriptState())
->SecurePaymentConfirmationService();
spc_service->MakePaymentCredential(
std::move(mojo_options),
BindOnce(&OnMakePublicKeyCredentialWithPaymentExtensionComplete,
std::make_unique<ScopedPromiseResolver>(resolver),
std::move(scoped_abort_state), std::move(feature_handle),
rp_id_for_payment_extension,
std::move(user_id_for_payment_extension)));
} else {
authenticator->MakeCredential(
std::move(mojo_options),
BindOnce(&OnMakePublicKeyCredentialWithPaymentExtensionComplete,
std::make_unique<ScopedPromiseResolver>(resolver),
std::move(scoped_abort_state), std::move(feature_handle),
rp_id_for_payment_extension,
std::move(user_id_for_payment_extension)));
}
} else {
if (RuntimeEnabledFeatures::WebAuthenticationConditionalCreateEnabled()) {
mojo_options->is_conditional =
options->mediation() ==
V8CredentialMediationRequirement::Enum::kConditional;
}
authenticator->MakeCredential(
std::move(mojo_options),
BindOnce(&OnMakePublicKeyCredentialComplete,
std::make_unique<ScopedPromiseResolver>(resolver),
std::move(scoped_abort_state), std::move(feature_handle),
required_origin_type, is_rk_required));
}
return promise;
}
ScriptPromise<IDLUndefined>
AuthenticationCredentialsContainer::preventSilentAccess(
ScriptState* script_state) {
if (!script_state->ContextIsValid()) {
return ScriptPromise<IDLUndefined>::RejectWithDOMException(
script_state,
MakeGarbageCollected<DOMException>(DOMExceptionCode::kInvalidStateError,
"Context is detached"));
}
auto* resolver =
MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(script_state);
auto 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(
BindOnce(&OnPreventSilentAccessComplete,
std::make_unique<ScopedPromiseResolver>(resolver)));
// TODO(https://crbug.com/1441075): Unify the implementation for
// different CredentialTypes and avoid the duplication eventually.
auto* auth_request =
CredentialManagerProxy::From(script_state)->FederatedAuthRequest();
auth_request->PreventSilentAccess(
BindOnce(&OnPreventSilentAccessComplete,
std::make_unique<ScopedPromiseResolver>(resolver)));
return promise;
}
void AuthenticationCredentialsContainer::Trace(Visitor* visitor) const {
Supplement<Navigator>::Trace(visitor);
CredentialsContainer::Trace(visitor);
}
void AuthenticationCredentialsContainer::ForwardRequestToAuthenticator(
ScriptState* script_state,
ScriptPromiseResolver<IDLNullable<Credential>>* resolver,
const CredentialRequestOptions* options) {
ExecutionContext* context = ExecutionContext::From(script_state);
std::unique_ptr<ScopedAbortState> scoped_abort_state = nullptr;
if (auto* signal = options->getSignalOr(nullptr)) {
auto* handle = signal->AddAlgorithm(
MakeGarbageCollected<PublicKeyRequestAbortAlgorithm>(script_state));
scoped_abort_state = std::make_unique<ScopedAbortState>(signal, handle);
}
Mediation mediation = Mediation::MODAL;
switch (options->mediation().AsEnum()) {
case V8CredentialMediationRequirement::Enum::kConditional:
UseCounter::Count(context, WebFeature::kWebAuthnConditionalUiGet);
CredentialMetrics::From(script_state).RecordWebAuthnConditionalUiCall();
mediation = Mediation::CONDITIONAL;
break;
case V8CredentialMediationRequirement::Enum::kImmediate:
if (RuntimeEnabledFeatures::WebAuthenticationImmediateGetEnabled(
context)) {
mediation = Mediation::IMMEDIATE;
EmitImmediateMediationUseCounters(context, options);
} else {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Immediate mediation not implemented"));
return;
}
break;
case V8CredentialMediationRequirement::Enum::kSilent:
case V8CredentialMediationRequirement::Enum::kOptional:
case V8CredentialMediationRequirement::Enum::kRequired:
break;
}
if (mediation == Mediation::IMMEDIATE) {
if (options->hasPublicKey() &&
!options->publicKey()->allowCredentials().empty()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"An allowCredentials is not allowed with immediate mediation."));
return;
}
if (options->hasPublicKey() && options->publicKey()->hasExtensions() &&
options->publicKey()->extensions()->hasRemoteDesktopClientOverride()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"Immediate mediation cannot be used with a remote desktop override "
"request."));
return;
}
if (!LocalFrame::ConsumeTransientUserActivation(
To<LocalDOMWindow>(resolver->GetExecutionContext())->GetFrame(),
UserActivationUpdateSource::kRenderer)) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"A user activation is required to request immediate credentials."));
return;
}
}
mojom::blink::GetCredentialOptionsPtr get_credential_options =
GetCredentialOptions::New();
get_credential_options->mediation = mediation;
if (options->hasPublicKey()) {
UseCounter::Count(context,
WebFeature::kCredentialManagerGetPublicKeyCredential);
#if BUILDFLAG(IS_ANDROID)
if (options->publicKey()->hasExtensions() &&
options->publicKey()->extensions()->hasUvm()) {
UseCounter::Count(context, WebFeature::kCredentialManagerGetWithUVM);
}
#endif
if (options->publicKey()->hasChallenge() &&
!IsArrayBufferOrViewBelowSizeLimit(options->publicKey()->challenge())) {
resolver->Reject(DOMException::Create(
"The `challenge` attribute exceeds the maximum allowed size.",
"RangeError"));
return;
}
if (!IsCredentialDescriptorListBelowSizeLimit(
options->publicKey()->allowCredentials())) {
resolver->Reject(
DOMException::Create("The `allowCredentials` attribute exceeds the "
"maximum allowed size (64).",
"RangeError"));
return;
}
if (options->publicKey()->hasExtensions()) {
if (options->publicKey()->extensions()->hasAppid()) {
const auto& appid = options->publicKey()->extensions()->appid();
if (!appid.empty()) {
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;
}
}
}
if (options->publicKey()->extensions()->credProps()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"The 'credProps' extension is only valid when creating "
"a credential"));
return;
}
if (options->publicKey()->extensions()->hasLargeBlob()) {
if (options->publicKey()->extensions()->largeBlob()->hasSupport()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"The 'largeBlob' extension's 'support' parameter is only valid "
"when creating a credential"));
return;
}
if (options->publicKey()->extensions()->largeBlob()->hasWrite()) {
const size_t write_size =
DOMArrayPiece(
options->publicKey()->extensions()->largeBlob()->write())
.ByteLength();
if (write_size > kMaxLargeBlobSize) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"The 'largeBlob' extension's 'write' parameter exceeds the "
"maximum allowed size (2kb)"));
return;
}
}
}
if (options->publicKey()->extensions()->hasPrf()) {
if (options->publicKey()->extensions()->prf()->hasEvalByCredential() &&
options->publicKey()->allowCredentials().empty()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"'prf' extension has 'evalByCredential' with an empty allow "
"list"));
return;
}
const char* error = validateGetPublicKeyCredentialPRFExtension(
*options->publicKey()->extensions()->prf(),
options->publicKey()->allowCredentials());
if (error != nullptr) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSyntaxError, error));
return;
}
// Prohibiting uv=preferred is omitted. See
// https://github.com/w3c/webauthn/pull/1836.
}
if (RuntimeEnabledFeatures::SecurePaymentConfirmationEnabled(context) &&
options->publicKey()->extensions()->hasPayment()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"The 'payment' extension is only valid when creating a "
"credential"));
return;
}
}
if (options->publicKey()->hasUserVerification() &&
!mojo::ConvertTo<
std::optional<mojom::blink::UserVerificationRequirement>>(
options->publicKey()->userVerification())) {
resolver->GetExecutionContext()->AddConsoleMessage(
MakeGarbageCollected<ConsoleMessage>(
mojom::blink::ConsoleMessageSource::kJavaScript,
mojom::blink::ConsoleMessageLevel::kWarning,
"Ignoring unknown publicKey.userVerification value"));
}
auto public_key_options =
MojoPublicKeyCredentialRequestOptions::From(*options->publicKey());
if (public_key_options) {
if (!public_key_options->relying_party_id) {
public_key_options->relying_party_id =
context->GetSecurityOrigin()->Domain();
}
get_credential_options->public_key = std::move(public_key_options);
} else {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Required parameters missing in 'options.publicKey'."));
return;
}
}
auto* authenticator =
CredentialManagerProxy::From(script_state)->Authenticator();
get_credential_options->password = options->password();
authenticator->GetCredential(
std::move(get_credential_options),
BindOnce(
&OnAuthenticatorGetCredentialComplete,
std::make_unique<ScopedPromiseResolver>(resolver),
std::move(scoped_abort_state),
ExecutionContext::From(script_state)
->GetScheduler()
->RegisterFeature(SchedulingPolicy::Feature::kWebAuthentication,
SchedulingPolicy::DisableBackForwardCache()),
mediation));
}
void AuthenticationCredentialsContainer::GetForIdentity(
ScriptState* script_state,
ScriptPromiseResolver<IDLNullable<Credential>>* resolver,
const CredentialRequestOptions& options,
const IdentityCredentialRequestOptions& identity_options) {
// Common errors for FedCM and WebIdentityDigitalCredential.
if (identity_options.providers().size() == 0) {
resolver->RejectWithTypeError("Need at least one identity provider.");
return;
}
ExecutionContext* context = ExecutionContext::From(script_state);
// TODO(https://crbug.com/1441075): Ideally the logic should be handled in
// CredentialManager via Get. However currently it's only for password
// management and we should refactor the logic to make it generic.
ContentSecurityPolicy* policy =
resolver->GetExecutionContext()
->GetContentSecurityPolicyForCurrentWorld();
if (identity_options.providers().size() > 1) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kFedCmMultipleIdentityProviders);
if (identity_options.providers().size() > 10u) {
resolver->RejectWithTypeError("More than 10 providers are not allowed.");
return;
}
}
// Log the UseCounter only when the WebID flag is enabled.
UseCounter::Count(context, WebFeature::kFedCm);
if (!To<LocalDOMWindow>(resolver->GetExecutionContext())
->GetFrame()
->IsMainFrame()) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kFedCmIframe);
}
int provider_index = 0;
Vector<mojom::blink::IdentityProviderRequestOptionsPtr>
identity_provider_ptrs;
for (const auto& provider : identity_options.providers()) {
if (provider->hasLoginHint()) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kFedCmLoginHint);
}
if (provider->hasDomainHint()) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kFedCmDomainHint);
}
mojom::blink::IdentityProviderRequestOptionsPtr identity_provider;
{
// It is possible that serializing the custom parameters to JSON fails
// due to a JS exception, e.g. a custom getter throwing an exception.
// Catch it here and rethrow so the caller knows what went wrong.
v8::TryCatch try_catch(script_state->GetIsolate());
identity_provider =
blink::mojom::blink::IdentityProviderRequestOptions::From(*provider);
if (!identity_provider) {
DCHECK(try_catch.HasCaught())
<< "Converting to mojo should only fail due to JS exception";
resolver->Reject(try_catch.Exception());
return;
}
}
if (blink::RuntimeEnabledFeatures::FedCmIdPRegistrationEnabled() &&
blink::RuntimeEnabledFeatures::FedCmMultipleIdentityProvidersEnabled(
context) &&
provider->configURL() == "any") {
identity_provider_ptrs.push_back(std::move(identity_provider));
continue;
}
// TODO(kenrb): Add some renderer-side validation here, such as
// validating |provider|, and making sure the calling context is legal.
// Some of this has not been spec'd yet.
KURL provider_url(provider->configURL());
if (!provider->hasClientId()) {
resolver->RejectWithTypeError("Missing the provider's clientId.");
return;
}
String client_id = provider->clientId();
++provider_index;
if (!provider_url.IsValid() || client_id.empty()) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError,
String::Format("Provider %i information is incomplete.",
provider_index)));
return;
}
// We disallow redirects (in idp_network_request_manager.cc), so it is
// enough to check the initial URL here.
if (IdentityCredential::IsRejectingPromiseDueToCSP(policy, resolver,
provider_url)) {
return;
}
identity_provider_ptrs.push_back(std::move(identity_provider));
}
mojom::blink::RpContext rp_context = mojom::blink::RpContext::kSignIn;
if (identity_options.hasContext()) {
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kFedCmRpContext);
rp_context =
mojo::ConvertTo<mojom::blink::RpContext>(identity_options.context());
}
base::UmaHistogramEnumeration("Blink.FedCm.RpContext", rp_context);
CredentialMediationRequirement mediation_requirement;
switch (options.mediation().AsEnum()) {
case V8CredentialMediationRequirement::Enum::kConditional:
if (RuntimeEnabledFeatures::FedCmAutofillEnabled()) {
mediation_requirement = CredentialMediationRequirement::kConditional;
} else {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"Conditional mediation is not supported for this credential type"));
return;
}
break;
case V8CredentialMediationRequirement::Enum::kSilent:
mediation_requirement = CredentialMediationRequirement::kSilent;
break;
case V8CredentialMediationRequirement::Enum::kRequired:
mediation_requirement = CredentialMediationRequirement::kRequired;
break;
case V8CredentialMediationRequirement::Enum::kOptional:
mediation_requirement = CredentialMediationRequirement::kOptional;
break;
case V8CredentialMediationRequirement::Enum::kImmediate:
NOTREACHED();
}
if (identity_options.hasMediation()) {
resolver->GetExecutionContext()->AddConsoleMessage(
MakeGarbageCollected<ConsoleMessage>(
mojom::blink::ConsoleMessageSource::kJavaScript,
mojom::blink::ConsoleMessageLevel::kWarning,
"The 'mediation' parameter should be used outside of 'identity' in "
"the FedCM API call."));
}
mojom::blink::RpMode rp_mode = mojom::blink::RpMode::kPassive;
auto v8_rp_mode = identity_options.mode();
rp_mode = mojo::ConvertTo<mojom::blink::RpMode>(v8_rp_mode);
if (rp_mode == mojom::blink::RpMode::kActive) {
if (identity_provider_ptrs.size() > 1u) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError,
"Active mode is not currently supported with multiple identity "
"providers."));
return;
}
if (mediation_requirement == CredentialMediationRequirement::kSilent) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"mediation:silent is not supported in active mode"));
return;
}
}
std::unique_ptr<ScopedAbortState> scoped_abort_state;
if (auto* signal = options.getSignalOr(nullptr)) {
// Checked signal->aborted() at the top of get().
auto callback =
BindOnce(&AbortIdentityCredentialRequest, WrapPersistent(script_state));
auto* handle = signal->AddAlgorithm(std::move(callback));
scoped_abort_state = std::make_unique<ScopedAbortState>(signal, handle);
}
Vector<mojom::blink::IdentityProviderGetParametersPtr> idp_get_params;
mojom::blink::IdentityProviderGetParametersPtr get_params =
mojom::blink::IdentityProviderGetParameters::New(
std::move(identity_provider_ptrs), rp_context, rp_mode);
idp_get_params.push_back(std::move(get_params));
auto* auth_request =
CredentialManagerProxy::From(script_state)->FederatedAuthRequest();
auth_request->RequestToken(
std::move(idp_get_params), mediation_requirement,
blink::BindOnce(&OnRequestToken,
std::make_unique<ScopedPromiseResolver>(resolver),
std::move(scoped_abort_state), WrapPersistent(&options)));
}
} // namespace blink