| // 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 |