| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/modules/credentialmanager/credentials_container.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/metrics/histogram_macros.h" |
| #include "base/notreached.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/rand_util.h" |
| #include "build/build_config.h" |
| #include "services/network/public/cpp/is_potentially_trustworthy.h" |
| #include "third_party/blink/public/common/sms/webotp_constants.h" |
| #include "third_party/blink/public/common/sms/webotp_service_outcome.h" |
| #include "third_party/blink/public/mojom/credentialmanager/credential_manager.mojom-blink.h" |
| #include "third_party/blink/public/mojom/payments/payment_credential.mojom-blink.h" |
| #include "third_party/blink/public/mojom/permissions_policy/permissions_policy.mojom-blink.h" |
| #include "third_party/blink/public/mojom/sms/webotp_service.mojom-blink.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_promise.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_union_arraybuffer_arraybufferview.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_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_federated_credential_request_options.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_federated_identity_provider.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_otp_credential_request_options.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_payment_credential_instrument.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_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_federatedidentityprovider_string.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/document.h" |
| #include "third_party/blink/renderer/core/dom/dom_exception.h" |
| #include "third_party/blink/renderer/core/execution_context/execution_context.h" |
| #include "third_party/blink/renderer/core/frame/frame.h" |
| #include "third_party/blink/renderer/core/frame/frame_console.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/frame/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/modules/credentialmanager/authenticator_assertion_response.h" |
| #include "third_party/blink/renderer/modules/credentialmanager/authenticator_attestation_response.h" |
| #include "third_party/blink/renderer/modules/credentialmanager/credential.h" |
| #include "third_party/blink/renderer/modules/credentialmanager/credential_manager_proxy.h" |
| #include "third_party/blink/renderer/modules/credentialmanager/credential_manager_type_converters.h" |
| #include "third_party/blink/renderer/modules/credentialmanager/federated_credential.h" |
| #include "third_party/blink/renderer/modules/credentialmanager/otp_credential.h" |
| #include "third_party/blink/renderer/modules/credentialmanager/password_credential.h" |
| #include "third_party/blink/renderer/modules/credentialmanager/public_key_credential.h" |
| #include "third_party/blink/renderer/modules/credentialmanager/scoped_promise_resolver.h" |
| #include "third_party/blink/renderer/platform/bindings/exception_state.h" |
| #include "third_party/blink/renderer/platform/heap/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/origin_access_entry.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/wtf_string.h" |
| #include "third_party/blink/renderer/platform/wtf/wtf_size_t.h" |
| |
| #if defined(OS_ANDROID) |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_public_key_credential_rp_entity.h" |
| #endif |
| |
| namespace blink { |
| |
| namespace { |
| |
| using mojom::blink::AuthenticatorStatus; |
| using mojom::blink::CredentialInfo; |
| using mojom::blink::CredentialInfoPtr; |
| using mojom::blink::CredentialManagerError; |
| using mojom::blink::CredentialMediationRequirement; |
| using mojom::blink::PaymentCredentialInstrument; |
| using MojoPublicKeyCredentialCreationOptions = |
| mojom::blink::PublicKeyCredentialCreationOptions; |
| using mojom::blink::MakeCredentialAuthenticatorResponsePtr; |
| using MojoPublicKeyCredentialRequestOptions = |
| mojom::blink::PublicKeyCredentialRequestOptions; |
| using mojom::blink::GetAssertionAuthenticatorResponsePtr; |
| using mojom::blink::RequestIdTokenStatus; |
| using mojom::blink::RequestMode; |
| using payments::mojom::blink::PaymentCredentialStorageStatus; |
| |
| constexpr char kCryptotokenOrigin[] = |
| "chrome-extension://kmendfapggjehodndflmmgagdbamhnfd"; |
| |
| // RequiredOriginType enumerates the requirements on the environment to perform |
| // an operation. |
| enum class RequiredOriginType { |
| // Must be a secure origin. |
| kSecure, |
| // Must be a secure origin and be same-origin with all ancestor frames. |
| kSecureAndSameWithAncestors, |
| // Must be a secure origin and the "publickey-credentials-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, |
| // Similar to the enum above, checks the "otp-credentials" permissions policy. |
| kSecureAndPermittedByWebOTPAssertionPermissionsPolicy, |
| // Must be a secure origin with allowed payment permission policy. |
| kSecureWithPaymentPermissionPolicy, |
| }; |
| |
| bool IsSameOriginWithAncestors(const Frame* frame) { |
| DCHECK(frame); |
| const Frame* current = frame; |
| const SecurityOrigin* origin = |
| frame->GetSecurityContext()->GetSecurityOrigin(); |
| while (current->Tree().Parent()) { |
| current = current->Tree().Parent(); |
| if (!origin->IsSameOriginWith( |
| current->GetSecurityContext()->GetSecurityOrigin())) |
| return false; |
| } |
| return true; |
| } |
| |
| // An ancestor chain is valid iff there are at most 2 unique origins on the |
| // chain (current origin included), the unique origins must be consecutive. |
| // e.g. the following are valid: |
| // A.com (calls WebOTP API) |
| // A.com -> A.com (calls WebOTP API) |
| // A.com -> A.com -> B.com (calls WebOTP API) |
| // A.com -> B.com -> B.com (calls WebOTP API) |
| // while the following are invalid: |
| // A.com -> B.com -> A.com (calls WebOTP API) |
| // A.com -> B.com -> C.com (calls WebOTP API) |
| // Note that there is additional requirement on feature permission being granted |
| // upon crossing origins but that is not verified by this function. |
| bool IsAncestorChainValidForWebOTP(const Frame* frame) { |
| const SecurityOrigin* current_origin = |
| frame->GetSecurityContext()->GetSecurityOrigin(); |
| int number_of_unique_origin = 1; |
| |
| const Frame* parent = frame->Tree().Parent(); |
| while (parent) { |
| auto* parent_origin = parent->GetSecurityContext()->GetSecurityOrigin(); |
| if (!parent_origin->IsSameOriginWith(current_origin)) { |
| ++number_of_unique_origin; |
| current_origin = parent_origin; |
| } |
| if (number_of_unique_origin > kMaxUniqueOriginInAncestorChainForWebOTP) |
| return false; |
| parent = parent->Tree().Parent(); |
| } |
| return true; |
| } |
| |
| bool CheckSecurityRequirementsBeforeRequest( |
| ScriptPromiseResolver* resolver, |
| RequiredOriginType required_origin_type) { |
| // Ignore calls if the current realm execution context is no longer valid, |
| // e.g., because the responsible document was detached. |
| DCHECK(resolver->GetExecutionContext()); |
| if (resolver->GetExecutionContext()->IsContextDestroyed()) { |
| resolver->Reject(); |
| return false; |
| } |
| |
| // The API is not exposed to Workers or Worklets, so if the current realm |
| // execution context is valid, it must have a responsible browsing context. |
| SECURITY_CHECK(resolver->DomWindow()); |
| |
| // The API is not exposed in non-secure context. |
| SECURITY_CHECK(resolver->GetExecutionContext()->IsSecureContext()); |
| |
| switch (required_origin_type) { |
| case RequiredOriginType::kSecure: |
| // This has already been checked. |
| break; |
| |
| case RequiredOriginType::kSecureAndSameWithAncestors: |
| if (!IsSameOriginWithAncestors(resolver->DomWindow()->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( |
| mojom::blink::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 { |
| UseCounter::Count( |
| resolver->GetExecutionContext(), |
| WebFeature::kCredentialManagerCrossOriginPublicKeyGetRequest); |
| } |
| break; |
| |
| case RequiredOriginType:: |
| kSecureAndPermittedByWebOTPAssertionPermissionsPolicy: |
| if (!resolver->GetExecutionContext()->IsFeatureEnabled( |
| mojom::blink::PermissionsPolicyFeature::kOTPCredentials)) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotAllowedError, |
| "The 'otp-credentials` feature is not enabled in this document.")); |
| return false; |
| } |
| if (!IsAncestorChainValidForWebOTP(resolver->DomWindow()->GetFrame())) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotAllowedError, |
| "More than two unique origins are detected in the origin chain.")); |
| return false; |
| } |
| break; |
| |
| case RequiredOriginType::kSecureWithPaymentPermissionPolicy: |
| if (!resolver->GetExecutionContext()->IsFeatureEnabled( |
| mojom::blink::PermissionsPolicyFeature::kPayment)) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "The 'payment' feature is 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( |
| ScriptPromiseResolver* resolver, |
| RequiredOriginType require_origin) { |
| // The |resolver| will blanket ignore Reject/Resolve calls if the context is |
| // gone -- nevertheless, call Reject() to be on the safe side. |
| if (!resolver->GetExecutionContext()) { |
| resolver->Reject(); |
| return; |
| } |
| |
| SECURITY_CHECK(resolver->DomWindow()); |
| SECURITY_CHECK(resolver->GetExecutionContext()->IsSecureContext()); |
| switch (require_origin) { |
| case RequiredOriginType::kSecure: |
| // This has already been checked. |
| break; |
| |
| case RequiredOriginType::kSecureAndSameWithAncestors: |
| SECURITY_CHECK( |
| IsSameOriginWithAncestors(resolver->DomWindow()->GetFrame())); |
| break; |
| |
| case RequiredOriginType:: |
| kSecureAndPermittedByWebAuthGetAssertionPermissionsPolicy: |
| SECURITY_CHECK(resolver->GetExecutionContext()->IsFeatureEnabled( |
| mojom::blink::PermissionsPolicyFeature::kPublicKeyCredentialsGet)); |
| break; |
| |
| case RequiredOriginType:: |
| kSecureAndPermittedByWebOTPAssertionPermissionsPolicy: |
| SECURITY_CHECK( |
| resolver->GetExecutionContext()->IsFeatureEnabled( |
| mojom::blink::PermissionsPolicyFeature::kOTPCredentials) && |
| IsAncestorChainValidForWebOTP(resolver->DomWindow()->GetFrame())); |
| break; |
| |
| case RequiredOriginType::kSecureWithPaymentPermissionPolicy: |
| SECURITY_CHECK(resolver->GetExecutionContext()->IsFeatureEnabled( |
| mojom::blink::PermissionsPolicyFeature::kPayment)); |
| 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(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; |
| switch (buffer_or_view->GetContentType()) { |
| case V8UnionArrayBufferOrArrayBufferView::ContentType::kArrayBuffer: |
| return base::CheckedNumeric<wtf_size_t>( |
| buffer_or_view->GetAsArrayBuffer()->ByteLength()) |
| .IsValid(); |
| case V8UnionArrayBufferOrArrayBufferView::ContentType::kArrayBufferView: |
| return base::CheckedNumeric<wtf_size_t>( |
| buffer_or_view->GetAsArrayBufferView()->byteLength()) |
| .IsValid(); |
| } |
| NOTREACHED(); |
| return false; |
| } |
| |
| 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::PENDING_REQUEST_WEBAUTHN: |
| // WebAuthn's PENDING_REQUEST is mapped to a different |
| // |CredentialManagerError| because WebAuthn wants kInvalidStateError to |
| // be distinctive so that sites can recognise it as |
| // |CREDENTIAL_EXCLUDED|. |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kOperationError, "A request is already pending."); |
| case CredentialManagerError::PASSWORD_STORE_UNAVAILABLE: |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "The password store is unavailable."); |
| case CredentialManagerError::NOT_ALLOWED: |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotAllowedError, |
| "The operation either timed out or was not allowed. See: " |
| "https://www.w3.org/TR/webauthn-2/" |
| "#sctn-privacy-considerations-client."); |
| case CredentialManagerError::INVALID_DOMAIN: |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kSecurityError, "This is an invalid domain."); |
| case CredentialManagerError::INVALID_ICON_URL: |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kSecurityError, "The icon should be a secure URL"); |
| case CredentialManagerError::CREDENTIAL_EXCLUDED: |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kInvalidStateError, |
| "The user attempted to register an authenticator that contains one " |
| "of the credentials already registered with the relying party."); |
| case CredentialManagerError::NOT_IMPLEMENTED: |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, "Not implemented"); |
| case CredentialManagerError::NOT_FOCUSED: |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotAllowedError, |
| "The operation is not allowed at this time " |
| "because the page does not have focus."); |
| case CredentialManagerError::RESIDENT_CREDENTIALS_UNSUPPORTED: |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "Resident credentials or empty " |
| "'allowCredentials' lists are not supported " |
| "at this time."); |
| case CredentialManagerError::PROTECTION_POLICY_INCONSISTENT: |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "Requested protection policy is inconsistent or incongruent with " |
| "other requested parameters."); |
| case CredentialManagerError::ANDROID_ALGORITHM_UNSUPPORTED: |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "None of the algorithms specified in " |
| "`pubKeyCredParams` are supported by " |
| "this device."); |
| case CredentialManagerError::ANDROID_EMPTY_ALLOW_CREDENTIALS: |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "Use of an empty `allowCredentials` list is " |
| "not supported on this device."); |
| case CredentialManagerError::ANDROID_NOT_SUPPORTED_ERROR: |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "Either the device has received unexpected " |
| "request parameters, or the device " |
| "cannot support this request."); |
| case CredentialManagerError::ANDROID_USER_VERIFICATION_UNSUPPORTED: |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "The specified `userVerification` " |
| "requirement cannot be fulfilled by " |
| "this device unless the device is secured " |
| "with a screen lock."); |
| case CredentialManagerError::ABORT: |
| return MakeGarbageCollected<DOMException>(DOMExceptionCode::kAbortError, |
| "Request has been aborted."); |
| case CredentialManagerError::OPAQUE_DOMAIN: |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotAllowedError, |
| "The current origin is an opaque origin and hence not allowed to " |
| "access 'PublicKeyCredential' objects."); |
| case CredentialManagerError::INVALID_PROTOCOL: |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kSecurityError, |
| "Public-key credentials are only available to HTTPS origin or HTTP " |
| "origins that fall under 'localhost'. See https://crbug.com/824383"); |
| case CredentialManagerError::BAD_RELYING_PARTY_ID: |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kSecurityError, |
| "The relying party ID is not a registrable domain suffix of, nor " |
| "equal to the current domain."); |
| case CredentialManagerError::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 CredentialManagerError::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 CredentialManagerError::UNKNOWN: |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotReadableError, |
| "An unknown error occurred while talking " |
| "to the credential manager."); |
| case CredentialManagerError:: |
| FAILED_TO_SAVE_CREDENTIAL_ID_FOR_PAYMENT_EXTENSION: |
| return MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotReadableError, |
| "Failed to save the credential identifier for the 'payment' " |
| "extension."); |
| case CredentialManagerError::SUCCESS: |
| NOTREACHED(); |
| break; |
| } |
| return nullptr; |
| } |
| |
| // Abort an ongoing PublicKeyCredential create() or get() operation. |
| void AbortPublicKeyRequest(ScriptState* script_state) { |
| if (!script_state->ContextIsValid()) |
| return; |
| |
| auto* authenticator = |
| CredentialManagerProxy::From(script_state)->Authenticator(); |
| authenticator->Cancel(); |
| } |
| |
| // Abort an ongoing OtpCredential get() operation. |
| void AbortOtpRequest(ScriptState* script_state) { |
| if (!script_state->ContextIsValid()) |
| return; |
| |
| auto* webotp_service = |
| CredentialManagerProxy::From(script_state)->WebOTPService(); |
| webotp_service->Abort(); |
| } |
| |
| // Abort an ongoing FederatedCredential get() operation. |
| void AbortFederatedCredentialRequest(ScriptState* script_state) { |
| if (!script_state->ContextIsValid()) |
| return; |
| |
| auto* fedcm_get_request = |
| CredentialManagerProxy::From(script_state)->FedCmGetRequest(); |
| fedcm_get_request->CancelTokenRequest(); |
| } |
| |
| void OnStoreComplete(std::unique_ptr<ScopedPromiseResolver> scoped_resolver) { |
| auto* resolver = scoped_resolver->Release(); |
| AssertSecurityRequirementsBeforeResponse( |
| resolver, RequiredOriginType::kSecureAndSameWithAncestors); |
| resolver->Resolve(); |
| } |
| |
| void OnPreventSilentAccessComplete( |
| std::unique_ptr<ScopedPromiseResolver> scoped_resolver) { |
| auto* resolver = scoped_resolver->Release(); |
| const auto required_origin_type = RequiredOriginType::kSecure; |
| AssertSecurityRequirementsBeforeResponse(resolver, required_origin_type); |
| |
| resolver->Resolve(); |
| } |
| |
| void OnGetComplete(std::unique_ptr<ScopedPromiseResolver> scoped_resolver, |
| RequiredOriginType required_origin_type, |
| CredentialManagerError error, |
| CredentialInfoPtr credential_info) { |
| auto* resolver = scoped_resolver->Release(); |
| |
| AssertSecurityRequirementsBeforeResponse(resolver, required_origin_type); |
| if (error != CredentialManagerError::SUCCESS) { |
| DCHECK(!credential_info); |
| resolver->Reject(CredentialManagerErrorToDOMException(error)); |
| return; |
| } |
| DCHECK(credential_info); |
| UseCounter::Count(resolver->GetExecutionContext(), |
| WebFeature::kCredentialManagerGetReturnedCredential); |
| resolver->Resolve(mojo::ConvertTo<Credential*>(std::move(credential_info))); |
| } |
| |
| DOMArrayBuffer* VectorToDOMArrayBuffer(const Vector<uint8_t> buffer) { |
| return DOMArrayBuffer::Create(static_cast<const void*>(buffer.data()), |
| buffer.size()); |
| } |
| |
| #if defined(OS_ANDROID) |
| Vector<Vector<uint32_t>> UvmEntryToArray( |
| const Vector<mojom::blink::UvmEntryPtr>& user_verification_methods) { |
| Vector<Vector<uint32_t>> uvm_array; |
| for (const auto& uvm : user_verification_methods) { |
| Vector<uint32_t> uvmEntry = {uvm->user_verification_method, |
| uvm->key_protection_type, |
| uvm->matcher_protection_type}; |
| uvm_array.push_back(uvmEntry); |
| } |
| return uvm_array; |
| } |
| #endif |
| |
| void OnMakePublicKeyCredentialComplete( |
| std::unique_ptr<ScopedPromiseResolver> scoped_resolver, |
| RequiredOriginType required_origin_type, |
| AuthenticatorStatus status, |
| MakeCredentialAuthenticatorResponsePtr credential) { |
| auto* resolver = scoped_resolver->Release(); |
| AssertSecurityRequirementsBeforeResponse(resolver, required_origin_type); |
| if (status != AuthenticatorStatus::SUCCESS) { |
| DCHECK(!credential); |
| resolver->Reject(CredentialManagerErrorToDOMException( |
| mojo::ConvertTo<CredentialManagerError>(status))); |
| return; |
| } |
| DCHECK(credential); |
| DCHECK(!credential->info->client_data_json.IsEmpty()); |
| DCHECK(!credential->attestation_object.IsEmpty()); |
| UseCounter::Count( |
| resolver->GetExecutionContext(), |
| WebFeature::kCredentialManagerMakePublicKeyCredentialSuccess); |
| DOMArrayBuffer* client_data_buffer = |
| VectorToDOMArrayBuffer(std::move(credential->info->client_data_json)); |
| DOMArrayBuffer* raw_id = |
| VectorToDOMArrayBuffer(std::move(credential->info->raw_id)); |
| DOMArrayBuffer* attestation_buffer = |
| VectorToDOMArrayBuffer(std::move(credential->attestation_object)); |
| 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) { |
| DCHECK(RuntimeEnabledFeatures:: |
| WebAuthenticationResidentKeyRequirementEnabled()); |
| 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) { |
| DCHECK( |
| RuntimeEnabledFeatures::WebAuthenticationLargeBlobExtensionEnabled()); |
| AuthenticationExtensionsLargeBlobOutputs* large_blob_outputs = |
| AuthenticationExtensionsLargeBlobOutputs::Create(); |
| large_blob_outputs->setSupported(credential->supports_large_blob); |
| extension_outputs->setLargeBlob(large_blob_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, |
| 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), |
| RequiredOriginType::kSecureWithPaymentPermissionPolicy, status, |
| std::move(credential)); |
| } |
| |
| void OnMakePublicKeyCredentialWithPaymentExtensionComplete( |
| std::unique_ptr<ScopedPromiseResolver> scoped_resolver, |
| const String& rp_id_for_payment_extension, |
| const WTF::Vector<uint8_t>& user_id_for_payment_extension, |
| AuthenticatorStatus status, |
| MakeCredentialAuthenticatorResponsePtr credential) { |
| auto* resolver = scoped_resolver->Release(); |
| const auto required_origin_type = |
| RequiredOriginType::kSecureWithPaymentPermissionPolicy; |
| |
| AssertSecurityRequirementsBeforeResponse(resolver, required_origin_type); |
| if (status != AuthenticatorStatus::SUCCESS) { |
| DCHECK(!credential); |
| resolver->Reject(CredentialManagerErrorToDOMException( |
| mojo::ConvertTo<CredentialManagerError>(status))); |
| return; |
| } |
| |
| Vector<uint8_t> credential_id = credential->info->raw_id; |
| auto* payment_credential_remote = |
| CredentialManagerProxy::From(resolver->GetScriptState()) |
| ->PaymentCredential(); |
| payment_credential_remote->StorePaymentCredential( |
| std::move(credential_id), rp_id_for_payment_extension, |
| std::move(user_id_for_payment_extension), |
| WTF::Bind(&OnSaveCredentialIdForPaymentExtension, |
| std::make_unique<ScopedPromiseResolver>(resolver), |
| std::move(credential))); |
| } |
| |
| void OnGetAssertionComplete( |
| std::unique_ptr<ScopedPromiseResolver> scoped_resolver, |
| AuthenticatorStatus status, |
| GetAssertionAuthenticatorResponsePtr credential) { |
| auto* resolver = scoped_resolver->Release(); |
| const auto required_origin_type = RequiredOriginType::kSecure; |
| |
| AssertSecurityRequirementsBeforeResponse(resolver, required_origin_type); |
| if (status == AuthenticatorStatus::SUCCESS) { |
| DCHECK(credential); |
| DCHECK(!credential->signature.IsEmpty()); |
| DCHECK(!credential->info->authenticator_data.IsEmpty()); |
| UseCounter::Count( |
| resolver->GetExecutionContext(), |
| WebFeature::kCredentialManagerGetPublicKeyCredentialSuccess); |
| 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 = |
| AuthenticationExtensionsClientOutputs::Create(); |
| if (credential->echo_appid_extension) { |
| extension_outputs->setAppid(credential->appid_extension); |
| } |
| #if defined(OS_ANDROID) |
| if (credential->echo_user_verification_methods) { |
| extension_outputs->setUvm( |
| UvmEntryToArray(std::move(*credential->user_verification_methods))); |
| UseCounter::Count(resolver->GetExecutionContext(), |
| WebFeature::kCredentialManagerGetSuccessWithUVM); |
| } |
| #endif |
| if (credential->echo_large_blob) { |
| DCHECK( |
| RuntimeEnabledFeatures::WebAuthenticationLargeBlobExtensionEnabled()); |
| AuthenticationExtensionsLargeBlobOutputs* large_blob_outputs = |
| AuthenticationExtensionsLargeBlobOutputs::Create(); |
| if (credential->large_blob) { |
| large_blob_outputs->setBlob( |
| VectorToDOMArrayBuffer(std::move(*credential->large_blob))); |
| } |
| if (credential->echo_large_blob_written) { |
| large_blob_outputs->setWritten(credential->large_blob_written); |
| } |
| extension_outputs->setLargeBlob(large_blob_outputs); |
| } |
| if (credential->echo_get_cred_blob) { |
| if (credential->get_cred_blob) { |
| extension_outputs->setGetCredBlob( |
| VectorToDOMArrayBuffer(std::move(*credential->get_cred_blob))); |
| } else { |
| extension_outputs->setGetCredBlob(nullptr); |
| } |
| } |
| resolver->Resolve(MakeGarbageCollected<PublicKeyCredential>( |
| credential->info->id, |
| VectorToDOMArrayBuffer(std::move(credential->info->raw_id)), |
| authenticator_response, credential->authenticator_attachment, |
| extension_outputs)); |
| return; |
| } |
| DCHECK(!credential); |
| resolver->Reject(CredentialManagerErrorToDOMException( |
| mojo::ConvertTo<CredentialManagerError>(status))); |
| } |
| |
| void OnSmsReceive(ScriptPromiseResolver* resolver, |
| base::TimeTicks start_time, |
| mojom::blink::SmsStatus status, |
| const String& otp) { |
| AssertSecurityRequirementsBeforeResponse( |
| resolver, resolver->GetExecutionContext()->IsFeatureEnabled( |
| mojom::blink::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) { |
| 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, |
| ScriptPromiseResolver* 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() != "required") || |
| (!authenticator->hasResidentKey() && |
| authenticator->hasRequireResidentKey() && |
| !authenticator->requireResidentKey())) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "A resident key is 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; |
| } |
| |
| RequestMode ToRequestMode(const String& mode) { |
| if (mode == "mediated") { |
| return RequestMode::kMediated; |
| } else { |
| return RequestMode::kPermission; |
| } |
| } |
| |
| void OnRequestIdToken(ScriptPromiseResolver* resolver, |
| RequestIdTokenStatus status, |
| const WTF::String& id_token) { |
| // TODO(yigu): we should reject certain promise with unified message and delay |
| // to avoid fingerprinting. |
| switch (status) { |
| case RequestIdTokenStatus::kApprovalDeclined: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kAbortError, "User declined the sign-in attempt.")); |
| return; |
| } |
| case RequestIdTokenStatus::kErrorTooManyRequests: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kAbortError, |
| "Only one navigator.credentials.get request may be outstanding at " |
| "one time.")); |
| return; |
| } |
| case RequestIdTokenStatus::kErrorFetchingWellKnownHttpNotFound: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNetworkError, |
| "The provider's .well-known configuration cannot be found.")); |
| return; |
| } |
| case RequestIdTokenStatus::kErrorFetchingWellKnownNoResponse: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNetworkError, |
| "The response body is empty when fetching the provider's .well-known " |
| "configuration.")); |
| return; |
| } |
| case RequestIdTokenStatus::kErrorFetchingWellKnownInvalidResponse: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kInvalidStateError, |
| "Provider's .well-known configuration is invalid.")); |
| return; |
| } |
| case RequestIdTokenStatus::kErrorFetchingClientIdMetadataHttpNotFound: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNetworkError, |
| "The provider's client metadata endpoint cannot be found.")); |
| return; |
| } |
| case RequestIdTokenStatus::kErrorFetchingClientIdMetadataNoResponse: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNetworkError, |
| "The response body is empty when fetching the provider's client " |
| "metadata.")); |
| return; |
| } |
| case RequestIdTokenStatus::kErrorFetchingClientIdMetadataInvalidResponse: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kInvalidStateError, |
| "Provider's client metadata is invalid.")); |
| return; |
| } |
| case RequestIdTokenStatus::kErrorFetchingSignin: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNetworkError, |
| "Error attempting to reach the provider's sign-in endpoint.")); |
| return; |
| } |
| case RequestIdTokenStatus::kErrorInvalidSigninResponse: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kInvalidStateError, |
| "Provider's sign-in response is invalid.")); |
| return; |
| } |
| case RequestIdTokenStatus::kErrorFetchingAccountsHttpNotFound: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNetworkError, |
| "The provider's accounts list endpoint cannot be found.")); |
| return; |
| } |
| case RequestIdTokenStatus::kErrorFetchingAccountsNoResponse: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNetworkError, |
| "The response body is empty when fetching the provider's accounts " |
| "list.")); |
| return; |
| } |
| case RequestIdTokenStatus::kErrorFetchingAccountsInvalidResponse: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kInvalidStateError, |
| "Provider's accounts list is invalid.")); |
| return; |
| } |
| case RequestIdTokenStatus::kErrorFetchingIdTokenHttpNotFound: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNetworkError, |
| "The provider's id token endpoint cannot be found.")); |
| return; |
| } |
| case RequestIdTokenStatus::kErrorFetchingIdTokenNoResponse: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNetworkError, |
| "The response body is empty when fetching the provider's id token.")); |
| return; |
| } |
| case RequestIdTokenStatus::kErrorFetchingIdTokenInvalidResponse: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kInvalidStateError, |
| "Provider's id token is invalid.")); |
| return; |
| } |
| case RequestIdTokenStatus::kErrorFetchingIdTokenInvalidRequest: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kInvalidStateError, |
| "The id token fetching request is invalid.")); |
| return; |
| } |
| case RequestIdTokenStatus::kErrorCanceled: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kAbortError, "The request has been aborted.")); |
| return; |
| } |
| case RequestIdTokenStatus::kError: { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNetworkError, "Error retrieving an id token.")); |
| return; |
| } |
| case RequestIdTokenStatus::kSuccess: { |
| resolver->Resolve(id_token); |
| return; |
| } |
| } |
| } |
| |
| } // namespace |
| |
| const char CredentialsContainer::kSupplementName[] = "CredentialsContainer"; |
| |
| CredentialsContainer* CredentialsContainer::credentials(Navigator& navigator) { |
| CredentialsContainer* credentials = |
| Supplement<Navigator>::From<CredentialsContainer>(navigator); |
| if (!credentials) { |
| credentials = MakeGarbageCollected<CredentialsContainer>(navigator); |
| ProvideTo(navigator, credentials); |
| } |
| return credentials; |
| } |
| |
| CredentialsContainer::CredentialsContainer(Navigator& navigator) |
| : Supplement<Navigator>(navigator) {} |
| |
| ScriptPromise CredentialsContainer::get( |
| ScriptState* script_state, |
| const CredentialRequestOptions* options) { |
| auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| |
| 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; |
| } |
| if (!CheckSecurityRequirementsBeforeRequest(resolver, required_origin_type)) { |
| return promise; |
| } |
| |
| if (options->hasFederated()) { |
| UseCounter::Count(resolver->GetExecutionContext(), |
| WebFeature::kCredentialManagerGetFederatedCredential); |
| } else if (options->hasPassword()) { |
| UseCounter::Count(resolver->GetExecutionContext(), |
| WebFeature::kCredentialManagerGetPasswordCredential); |
| } |
| |
| if (options->hasPublicKey()) { |
| auto cryptotoken_origin = SecurityOrigin::Create(KURL(kCryptotokenOrigin)); |
| if (!cryptotoken_origin->IsSameOriginWith( |
| resolver->GetExecutionContext()->GetSecurityOrigin())) { |
| // Cryptotoken requests are recorded as kU2FCryptotokenSign from within |
| // the extension. |
| UseCounter::Count(resolver->GetExecutionContext(), |
| WebFeature::kCredentialManagerGetPublicKeyCredential); |
| } |
| |
| #if defined(OS_ANDROID) |
| if (options->publicKey()->hasExtensions() && |
| options->publicKey()->extensions()->hasUvm()) { |
| UseCounter::Count(resolver->GetExecutionContext(), |
| WebFeature::kCredentialManagerGetWithUVM); |
| } |
| #endif |
| |
| if (!IsArrayBufferOrViewBelowSizeLimit(options->publicKey()->challenge())) { |
| resolver->Reject(DOMException::Create( |
| "The `challenge` attribute exceeds the maximum allowed size.", |
| "RangeError")); |
| return promise; |
| } |
| |
| if (!IsCredentialDescriptorListBelowSizeLimit( |
| options->publicKey()->allowCredentials())) { |
| resolver->Reject( |
| DOMException::Create("The `allowCredentials` attribute exceeds the " |
| "maximum allowed size (64).", |
| "RangeError")); |
| return promise; |
| } |
| |
| if (options->publicKey()->hasExtensions()) { |
| if (options->publicKey()->extensions()->hasAppid()) { |
| const auto& appid = options->publicKey()->extensions()->appid(); |
| if (!appid.IsEmpty()) { |
| KURL appid_url(appid); |
| if (!appid_url.IsValid()) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kSyntaxError, |
| "The `appid` extension value is neither " |
| "empty/null nor a valid URL")); |
| return promise; |
| } |
| } |
| } |
| if (options->publicKey()->extensions()->hasCableRegistration()) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "The 'cableRegistration' extension is only valid when creating " |
| "a credential")); |
| return promise; |
| } |
| if (options->publicKey()->extensions()->credProps()) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "The 'credProps' extension is only valid when creating " |
| "a credential")); |
| return promise; |
| } |
| if (options->publicKey()->extensions()->hasLargeBlob()) { |
| DCHECK(RuntimeEnabledFeatures:: |
| WebAuthenticationLargeBlobExtensionEnabled()); |
| 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 promise; |
| } |
| } |
| if (options->publicKey()->extensions()->hasGoogleLegacyAppidSupport()) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "The 'googleLegacyAppidSupport' extension is only valid when " |
| "creating a credential")); |
| return promise; |
| } |
| if (RuntimeEnabledFeatures::SecurePaymentConfirmationEnabled( |
| resolver->GetExecutionContext()) && |
| options->publicKey()->extensions()->hasPayment()) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotAllowedError, |
| "The 'payment' extension is only valid when creating a " |
| "credential")); |
| return promise; |
| } |
| } |
| |
| if (!options->publicKey()->hasUserVerification()) { |
| resolver->DomWindow()->AddConsoleMessage( |
| MakeGarbageCollected<ConsoleMessage>( |
| mojom::blink::ConsoleMessageSource::kJavaScript, |
| mojom::blink::ConsoleMessageLevel::kWarning, |
| "publicKey.userVerification was not set to any value in Web " |
| "Authentication navigator.credentials.get() call. This defaults " |
| "to " |
| "'preferred', which is probably not what you want. If in doubt, " |
| "set " |
| "to 'discouraged'. See " |
| "https://chromium.googlesource.com/chromium/src/+/master/content/" |
| "browser/webauth/uv_preferred.md for details.")); |
| } |
| |
| if (options->hasSignal()) { |
| if (options->signal()->aborted()) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kAbortError, "Request has been aborted.")); |
| return promise; |
| } |
| options->signal()->AddAlgorithm( |
| WTF::Bind(&AbortPublicKeyRequest, WrapPersistent(script_state))); |
| } |
| |
| bool is_conditional_ui_request = conditionalMediationSupported() && |
| options->mediation() == "conditional"; |
| if (is_conditional_ui_request && |
| options->publicKey()->hasAllowCredentials() && |
| !options->publicKey()->allowCredentials().IsEmpty()) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotAllowedError, |
| "allowCredentials is not supported for conditionalPublicKey")); |
| return promise; |
| } |
| |
| auto mojo_options = |
| MojoPublicKeyCredentialRequestOptions::From(*options->publicKey()); |
| if (mojo_options) { |
| mojo_options->is_conditional = is_conditional_ui_request; |
| if (!mojo_options->relying_party_id) { |
| mojo_options->relying_party_id = |
| resolver->GetExecutionContext()->GetSecurityOrigin()->Domain(); |
| } |
| auto* authenticator = |
| CredentialManagerProxy::From(script_state)->Authenticator(); |
| authenticator->GetAssertion( |
| std::move(mojo_options), |
| WTF::Bind(&OnGetAssertionComplete, |
| std::make_unique<ScopedPromiseResolver>(resolver))); |
| } else { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "Required parameters missing in 'options.publicKey'.")); |
| } |
| return promise; |
| } |
| |
| if (options->hasOtp() && options->otp()->hasTransport()) { |
| if (!options->otp()->transport().Contains("sms")) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "Unsupported transport type for OTP Credentials")); |
| return promise; |
| } |
| |
| if (options->hasSignal()) { |
| if (options->signal()->aborted()) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kAbortError, "Request has been aborted.")); |
| return promise; |
| } |
| options->signal()->AddAlgorithm( |
| WTF::Bind(&AbortOtpRequest, WrapPersistent(script_state))); |
| } |
| |
| auto* webotp_service = |
| CredentialManagerProxy::From(script_state)->WebOTPService(); |
| webotp_service->Receive(WTF::Bind(&OnSmsReceive, WrapPersistent(resolver), |
| base::TimeTicks::Now())); |
| UMA_HISTOGRAM_ENUMERATION("Blink.UseCounter.Features", WebFeature::kWebOTP); |
| return promise; |
| } |
| |
| Vector<KURL> providers; |
| if (options->hasFederated() && options->federated()->hasProviders()) { |
| for (const auto& provider : options->federated()->providers()) { |
| if (provider->IsString()) { |
| KURL url = KURL(NullURL(), provider->GetAsString()); |
| if (url.IsValid()) |
| providers.push_back(std::move(url)); |
| } else if (provider->IsFederatedIdentityProvider()) { |
| // TODO(yigu): 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. |
| if (!RuntimeEnabledFeatures::WebIDEnabled()) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, "Invalid provider entry")); |
| return promise; |
| } |
| // 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. |
| FederatedIdentityProvider* federated_identity_provider = |
| provider->GetAsFederatedIdentityProvider(); |
| KURL provider_url(federated_identity_provider->url()); |
| String client_id = federated_identity_provider->clientId(); |
| String nonce = federated_identity_provider->hasNonce() |
| ? federated_identity_provider->nonce() |
| : ""; |
| if (!provider_url.IsValid() || client_id == "") { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kInvalidStateError, |
| "Provided provider information is incomplete.")); |
| return promise; |
| } |
| DCHECK(options->federated()->hasPreferAutoSignIn()); |
| if (options->hasSignal()) { |
| if (options->signal()->aborted()) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kAbortError, "Request has been aborted.")); |
| return promise; |
| } |
| options->signal()->AddAlgorithm(WTF::Bind( |
| &AbortFederatedCredentialRequest, WrapPersistent(script_state))); |
| } |
| bool prefer_auto_sign_in = options->federated()->preferAutoSignIn(); |
| auto* fedcm_get_request = |
| CredentialManagerProxy::From(script_state)->FedCmGetRequest(); |
| fedcm_get_request->RequestIdToken( |
| provider_url, client_id, nonce, |
| ToRequestMode(options->federated()->mode()), prefer_auto_sign_in, |
| WTF::Bind(&OnRequestIdToken, WrapPersistent(resolver))); |
| return promise; |
| } |
| } |
| } |
| |
| CredentialMediationRequirement requirement; |
| if (options->mediation() == "conditional") { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "Conditional mediation is not supported for this credential type")); |
| return promise; |
| } |
| if (options->mediation() == "silent") { |
| UseCounter::Count(ExecutionContext::From(script_state), |
| WebFeature::kCredentialManagerGetMediationSilent); |
| requirement = CredentialMediationRequirement::kSilent; |
| } else if (options->mediation() == "optional") { |
| UseCounter::Count(ExecutionContext::From(script_state), |
| WebFeature::kCredentialManagerGetMediationOptional); |
| requirement = CredentialMediationRequirement::kOptional; |
| } else { |
| DCHECK_EQ("required", options->mediation()); |
| UseCounter::Count(ExecutionContext::From(script_state), |
| WebFeature::kCredentialManagerGetMediationRequired); |
| requirement = CredentialMediationRequirement::kRequired; |
| } |
| |
| auto* credential_manager = |
| CredentialManagerProxy::From(script_state)->CredentialManager(); |
| credential_manager->Get( |
| requirement, options->password(), std::move(providers), |
| WTF::Bind(&OnGetComplete, |
| std::make_unique<ScopedPromiseResolver>(resolver), |
| required_origin_type)); |
| |
| return promise; |
| } |
| |
| ScriptPromise CredentialsContainer::store(ScriptState* script_state, |
| Credential* credential) { |
| auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| |
| if (!(credential->IsFederatedCredential() || |
| credential->IsPasswordCredential())) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "Store operation not permitted for PublicKey credentials.")); |
| return promise; |
| } |
| |
| if (!CheckSecurityRequirementsBeforeRequest( |
| resolver, RequiredOriginType::kSecureAndSameWithAncestors)) { |
| return promise; |
| } |
| |
| 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(); |
| credential_manager->Store( |
| CredentialInfo::From(credential), |
| WTF::Bind(&OnStoreComplete, |
| std::make_unique<ScopedPromiseResolver>(resolver))); |
| |
| return promise; |
| } |
| |
| ScriptPromise CredentialsContainer::create( |
| ScriptState* script_state, |
| const CredentialCreationOptions* options, |
| ExceptionState& exception_state) { |
| auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| |
| RequiredOriginType required_origin_type; |
| if (IsForPayment(options, resolver->GetExecutionContext())) { |
| required_origin_type = |
| RequiredOriginType::kSecureWithPaymentPermissionPolicy; |
| } else { |
| // hasPublicKey() implies that this is a WebAuthn request. |
| required_origin_type = options->hasPublicKey() |
| ? RequiredOriginType::kSecureAndSameWithAncestors |
| : RequiredOriginType::kSecure; |
| } |
| if (!CheckSecurityRequirementsBeforeRequest(resolver, required_origin_type)) { |
| return promise; |
| } |
| |
| if ((options->hasPassword() + options->hasFederated() + |
| options->hasPublicKey()) != 1) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "Only exactly one of 'password', 'federated', and 'publicKey' " |
| "credential types are currently supported.")); |
| return promise; |
| } |
| |
| if (options->hasPassword()) { |
| 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()); |
| auto cryptotoken_origin = SecurityOrigin::Create(KURL(kCryptotokenOrigin)); |
| if (!cryptotoken_origin->IsSameOriginWith( |
| resolver->GetExecutionContext()->GetSecurityOrigin())) { |
| // Cryptotoken requests are recorded as kU2FCryptotokenRegister from |
| // within the extension. |
| 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.IsEmpty()) { |
| KURL appid_exclude_url(appid_exclude); |
| if (!appid_exclude_url.IsValid()) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kSyntaxError, |
| "The `appidExclude` extension value is neither " |
| "empty/null nor a valid URL.")); |
| return promise; |
| } |
| } |
| } |
| if (options->publicKey()->extensions()->hasCableAuthentication()) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "The 'cableAuthentication' extension is only valid when requesting " |
| "an assertion")); |
| return promise; |
| } |
| if (options->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()->hasGoogleLegacyAppidSupport()) { |
| const auto& rp_id = |
| options->publicKey()->rp()->id() |
| ? options->publicKey()->rp()->id() |
| : resolver->GetExecutionContext()->GetSecurityOrigin()->Domain(); |
| if (rp_id != "google.com") { |
| resolver->DomWindow()->AddConsoleMessage( |
| MakeGarbageCollected<ConsoleMessage>( |
| mojom::blink::ConsoleMessageSource::kJavaScript, |
| mojom::blink::ConsoleMessageLevel::kWarning, |
| "The 'googleLegacyAppidSupport' extension is ignored for " |
| "requests with an 'rp.id' not equal to 'google.com'")); |
| } |
| } |
| if (options->publicKey()->extensions()->hasPayment() && |
| !IsPaymentExtensionValid(options, resolver)) { |
| return promise; |
| } |
| } |
| |
| if (options->hasSignal()) { |
| if (options->signal()->aborted()) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kAbortError, "Request has been aborted.")); |
| return promise; |
| } |
| options->signal()->AddAlgorithm( |
| WTF::Bind(&AbortPublicKeyRequest, WrapPersistent(script_state))); |
| } |
| |
| if (options->publicKey()->hasAuthenticatorSelection() && |
| !options->publicKey()->authenticatorSelection()->hasUserVerification()) { |
| resolver->DomWindow()->AddConsoleMessage( |
| MakeGarbageCollected<ConsoleMessage>( |
| mojom::blink::ConsoleMessageSource::kJavaScript, |
| mojom::blink::ConsoleMessageLevel::kWarning, |
| "publicKey.authenticatorSelection.userVerification was not set " |
| "to " |
| "any value in Web Authentication navigator.credentials.create() " |
| "call. This defaults to 'preferred', which is probably not what " |
| "you " |
| "want. If in doubt, set to 'discouraged'. See " |
| "https://chromium.googlesource.com/chromium/src/+/master/content/" |
| "browser/webauth/uv_preferred.md for details")); |
| } |
| if (options->publicKey()->hasAuthenticatorSelection() && |
| options->publicKey()->authenticatorSelection()->hasResidentKey() && |
| !mojo::ConvertTo<absl::optional<mojom::blink::ResidentKeyRequirement>>( |
| options->publicKey()->authenticatorSelection()->residentKey())) { |
| resolver->DomWindow()->AddConsoleMessage( |
| MakeGarbageCollected<ConsoleMessage>( |
| mojom::blink::ConsoleMessageSource::kJavaScript, |
| mojom::blink::ConsoleMessageLevel::kWarning, |
| "Ignoring unknown publicKey.authenticatorSelection.residentKey " |
| "value")); |
| } |
| |
| auto mojo_options = |
| MojoPublicKeyCredentialCreationOptions::From(*options->publicKey()); |
| if (!mojo_options) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kNotSupportedError, |
| "Required parameters missing in `options.publicKey`.")); |
| } else if (mojo_options->user->id.size() > 64) { |
| // https://www.w3.org/TR/webauthn/#user-handle |
| v8::Isolate* isolate = resolver->GetScriptState()->GetIsolate(); |
| resolver->Reject(V8ThrowException::CreateTypeError( |
| isolate, "User handle exceeds 64 bytes.")); |
| } else { |
| if (!mojo_options->relying_party->id) { |
| mojo_options->relying_party->id = |
| resolver->GetExecutionContext()->GetSecurityOrigin()->Domain(); |
| } |
| |
| if (mojo_options->relying_party->icon) { |
| if (!IsIconURLNullOrSecure(mojo_options->relying_party->icon.value())) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kSecurityError, |
| "'rp.icon' should be a secure URL")); |
| return promise; |
| } |
| } |
| |
| if (mojo_options->user->icon) { |
| if (!IsIconURLNullOrSecure(mojo_options->user->icon.value())) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kSecurityError, |
| "'user.icon' should be a secure URL")); |
| return promise; |
| } |
| } |
| |
| auto* authenticator = |
| CredentialManagerProxy::From(script_state)->Authenticator(); |
| if (mojo_options->is_payment_credential_creation) { |
| String rp_id_for_payment_extension = mojo_options->relying_party->id; |
| WTF::Vector<uint8_t> user_id_for_payment_extension = |
| mojo_options->user->id; |
| authenticator->MakeCredential( |
| std::move(mojo_options), |
| WTF::Bind(&OnMakePublicKeyCredentialWithPaymentExtensionComplete, |
| std::make_unique<ScopedPromiseResolver>(resolver), |
| rp_id_for_payment_extension, |
| std::move(user_id_for_payment_extension))); |
| } else { |
| authenticator->MakeCredential( |
| std::move(mojo_options), |
| WTF::Bind(&OnMakePublicKeyCredentialComplete, |
| std::make_unique<ScopedPromiseResolver>(resolver), |
| required_origin_type)); |
| } |
| } |
| |
| return promise; |
| } |
| |
| ScriptPromise CredentialsContainer::preventSilentAccess( |
| ScriptState* script_state) { |
| auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| const auto required_origin_type = RequiredOriginType::kSecure; |
| if (!CheckSecurityRequirementsBeforeRequest(resolver, required_origin_type)) { |
| return promise; |
| } |
| |
| auto* credential_manager = |
| CredentialManagerProxy::From(script_state)->CredentialManager(); |
| credential_manager->PreventSilentAccess( |
| WTF::Bind(&OnPreventSilentAccessComplete, |
| std::make_unique<ScopedPromiseResolver>(resolver))); |
| |
| return promise; |
| } |
| |
| bool CredentialsContainer::conditionalMediationSupported() { |
| return RuntimeEnabledFeatures::WebAuthenticationConditionalUIEnabled(); |
| } |
| |
| void CredentialsContainer::Trace(Visitor* visitor) const { |
| ScriptWrappable::Trace(visitor); |
| Supplement<Navigator>::Trace(visitor); |
| } |
| |
| } // namespace blink |