| // 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 "third_party/blink/public/platform/modules/credentialmanager/credential_manager.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/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/local_frame.h" |
| #include "third_party/blink/renderer/core/frame/use_counter.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_creation_options.h" |
| #include "third_party/blink/renderer/modules/credentialmanager/credential_manager_proxy.h" |
| #include "third_party/blink/renderer/modules/credentialmanager/credential_manager_type_converters.h" |
| #include "third_party/blink/renderer/modules/credentialmanager/credential_request_options.h" |
| #include "third_party/blink/renderer/modules/credentialmanager/federated_credential.h" |
| #include "third_party/blink/renderer/modules/credentialmanager/federated_credential_request_options.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/public_key_credential_creation_options.h" |
| #include "third_party/blink/renderer/modules/credentialmanager/public_key_credential_request_options.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/weborigin/origin_access_entry.h" |
| #include "third_party/blink/renderer/platform/weborigin/security_origin.h" |
| #include "third_party/blink/renderer/platform/wtf/functional.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| using mojom::blink::CredentialManagerError; |
| using mojom::blink::CredentialInfo; |
| using mojom::blink::CredentialInfoPtr; |
| using mojom::blink::CredentialMediationRequirement; |
| using mojom::blink::AuthenticatorStatus; |
| using MojoPublicKeyCredentialCreationOptions = |
| mojom::blink::PublicKeyCredentialCreationOptions; |
| using mojom::blink::MakeCredentialAuthenticatorResponsePtr; |
| using MojoPublicKeyCredentialRequestOptions = |
| mojom::blink::PublicKeyCredentialRequestOptions; |
| using mojom::blink::GetAssertionAuthenticatorResponsePtr; |
| |
| constexpr char kCryptotokenOrigin[] = |
| "chrome-extension://kmendfapggjehodndflmmgagdbamhnfd"; |
| enum class RequiredOriginType { kSecure, kSecureAndSameWithAncestors }; |
| |
| bool IsSameOriginWithAncestors(const Frame* frame) { |
| DCHECK(frame); |
| const Frame* current = frame; |
| const SecurityOrigin* origin = |
| frame->GetSecurityContext()->GetSecurityOrigin(); |
| while (current->Tree().Parent()) { |
| current = current->Tree().Parent(); |
| if (!origin->CanAccess(current->GetSecurityContext()->GetSecurityOrigin())) |
| return false; |
| } |
| return true; |
| } |
| |
| bool CheckSecurityRequirementsBeforeRequest( |
| ScriptPromiseResolver* resolver, |
| RequiredOriginType required_origin_type) { |
| // Ignore calls if the current realm execution context is no longer valid, |
| // e.g., because the responsible document was detached. |
| DCHECK(resolver->GetExecutionContext()); |
| if (resolver->GetExecutionContext()->IsContextDestroyed()) { |
| resolver->Reject(); |
| return false; |
| } |
| |
| // The API is not exposed to Workers or Worklets, so if the current realm |
| // execution context is valid, it must have a responsible browsing context. |
| SECURITY_CHECK(resolver->GetFrame()); |
| |
| // The API is not exposed in non-secure context. |
| SECURITY_CHECK(resolver->GetExecutionContext()->IsSecureContext()); |
| |
| if (required_origin_type == RequiredOriginType::kSecureAndSameWithAncestors && |
| !IsSameOriginWithAncestors(resolver->GetFrame())) { |
| resolver->Reject(DOMException::Create( |
| 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', " |
| "and creation/retrieval of 'PublicKeyCredential'")); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void AssertSecurityRequirementsBeforeResponse( |
| ScriptPromiseResolver* resolver, |
| RequiredOriginType require_origin) { |
| // The |resolver| will blanket ignore Reject/Resolve calls if the context is |
| // gone -- nevertheless, call Reject() to be on the safe side. |
| if (!resolver->GetExecutionContext()) { |
| resolver->Reject(); |
| return; |
| } |
| |
| SECURITY_CHECK(resolver->GetFrame()); |
| SECURITY_CHECK(resolver->GetExecutionContext()->IsSecureContext()); |
| SECURITY_CHECK(require_origin != |
| RequiredOriginType::kSecureAndSameWithAncestors || |
| IsSameOriginWithAncestors(resolver->GetFrame())); |
| } |
| |
| bool CheckPublicKeySecurityRequirements(ScriptPromiseResolver* resolver, |
| const String& relying_party_id) { |
| const SecurityOrigin* origin = |
| resolver->GetFrame()->GetSecurityContext()->GetSecurityOrigin(); |
| |
| if (origin->IsOpaque()) { |
| String error_message = |
| "The origin ' " + origin->ToRawString() + |
| "' is an opaque origin and hence not allowed to access " + |
| "'PublicKeyCredential' objects."; |
| resolver->Reject(DOMException::Create(DOMExceptionCode::kNotAllowedError, |
| error_message)); |
| return false; |
| } |
| |
| auto cryptotoken_origin = SecurityOrigin::Create(KURL(kCryptotokenOrigin)); |
| if (cryptotoken_origin->IsSameSchemeHostPort(origin)) { |
| // Allow CryptoToken U2F extension to assert any origin, as cryptotoken |
| // handles origin checking separately. |
| return true; |
| } |
| |
| if (origin->Protocol() != url::kHttpScheme && |
| origin->Protocol() != url::kHttpsScheme) { |
| resolver->Reject(DOMException::Create( |
| DOMExceptionCode::kNotAllowedError, |
| "Public-key credentials are only available to HTTPS origin or " |
| "HTTP origins that fall under 'localhost'. See " |
| "https://crbug.com/824383")); |
| return false; |
| } |
| |
| DCHECK_NE(origin->Protocol(), url::kAboutScheme); |
| DCHECK_NE(origin->Protocol(), url::kFileScheme); |
| |
| // Validate the effective domain. |
| // For step 6 of both |
| // https://w3c.github.io/webauthn/#createCredential and |
| // https://w3c.github.io/webauthn/#discover-from-external-source. |
| String effective_domain = origin->Domain(); |
| |
| // TODO(crbug.com/803077): Avoid constructing an OriginAccessEntry just |
| // for the IP address check. |
| bool reject_because_invalid_domain = effective_domain.IsEmpty(); |
| if (!reject_because_invalid_domain) { |
| OriginAccessEntry access_entry( |
| origin->Protocol(), effective_domain, |
| network::mojom::CorsOriginAccessMatchMode::kAllowSubdomains); |
| reject_because_invalid_domain = access_entry.HostIsIPAddress(); |
| } |
| if (reject_because_invalid_domain) { |
| resolver->Reject( |
| DOMException::Create(DOMExceptionCode::kSecurityError, |
| "Effective domain is not a valid domain.")); |
| return false; |
| } |
| |
| // For the steps detailed in |
| // https://w3c.github.io/webauthn/#CreateCred-DetermineRpId and |
| // https://w3c.github.io/webauthn/#GetAssn-DetermineRpId. |
| if (!relying_party_id.IsNull()) { |
| OriginAccessEntry access_entry( |
| origin->Protocol(), relying_party_id, |
| network::mojom::CorsOriginAccessMatchMode::kAllowSubdomains); |
| if (relying_party_id.IsEmpty() || |
| access_entry.MatchesDomain(*origin) != |
| network::cors::OriginAccessEntry::kMatchesOrigin) { |
| resolver->Reject(DOMException::Create( |
| DOMExceptionCode::kSecurityError, |
| "The relying party ID '" + relying_party_id + |
| "' is not a registrable domain suffix of, nor equal to '" + |
| origin->ToRawString() + "'.")); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| // Checks if the icon URL of |credential| is an a-priori authenticated URL. |
| // https://w3c.github.io/webappsec-credential-management/#dom-credentialuserdata-iconurl |
| bool IsIconURLEmptyOrSecure(const Credential* credential) { |
| if (!credential->IsPasswordCredential() && |
| !credential->IsFederatedCredential()) { |
| DCHECK(credential->IsPublicKeyCredential()); |
| return true; |
| } |
| |
| const KURL& url = |
| credential->IsFederatedCredential() |
| ? static_cast<const FederatedCredential*>(credential)->iconURL() |
| : static_cast<const PasswordCredential*>(credential)->iconURL(); |
| if (url.IsEmpty()) |
| return true; |
| |
| // https://www.w3.org/TR/mixed-content/#a-priori-authenticated-url |
| return url.IsAboutSrcdocURL() || url.IsAboutBlankURL() || |
| url.ProtocolIsData() || |
| SecurityOrigin::Create(url)->IsPotentiallyTrustworthy(); |
| } |
| |
| DOMException* CredentialManagerErrorToDOMException( |
| CredentialManagerError reason) { |
| switch (reason) { |
| case CredentialManagerError::PENDING_REQUEST: |
| return DOMException::Create(DOMExceptionCode::kInvalidStateError, |
| "A request is already pending."); |
| case CredentialManagerError::PASSWORD_STORE_UNAVAILABLE: |
| return DOMException::Create(DOMExceptionCode::kNotSupportedError, |
| "The password store is unavailable."); |
| case CredentialManagerError::NOT_ALLOWED: |
| return DOMException::Create( |
| DOMExceptionCode::kNotAllowedError, |
| "The operation either timed out or was not allowed. See: " |
| "https://w3c.github.io/webauthn/#sec-assertion-privacy."); |
| case CredentialManagerError::INVALID_DOMAIN: |
| return DOMException::Create(DOMExceptionCode::kSecurityError, |
| "This is an invalid domain."); |
| case CredentialManagerError::CREDENTIAL_EXCLUDED: |
| return DOMException::Create( |
| DOMExceptionCode::kInvalidStateError, |
| "The user attempted to register an authenticator that contains one " |
| "of the credentials already registered with the relying party."); |
| case CredentialManagerError::CREDENTIAL_NOT_RECOGNIZED: |
| return DOMException::Create(DOMExceptionCode::kInvalidStateError, |
| "The user attempted to use an authenticator " |
| "that recognized none of the provided " |
| "credentials."); |
| case CredentialManagerError::NOT_IMPLEMENTED: |
| return DOMException::Create(DOMExceptionCode::kNotSupportedError, |
| "Not implemented"); |
| case CredentialManagerError::NOT_FOCUSED: |
| return DOMException::Create(DOMExceptionCode::kNotAllowedError, |
| "The operation is not allowed at this time " |
| "because the page does not have focus."); |
| case CredentialManagerError::RESIDENT_CREDENTIALS_UNSUPPORTED: |
| return DOMException::Create(DOMExceptionCode::kNotSupportedError, |
| "Resident credentials or empty " |
| "'allowCredentials' lists are not supported " |
| "at this time."); |
| case CredentialManagerError::ANDROID_ALGORITHM_UNSUPPORTED: |
| return DOMException::Create(DOMExceptionCode::kNotSupportedError, |
| "None of the algorithms specified in " |
| "`pubKeyCredParams` are supported by " |
| "this device."); |
| case CredentialManagerError::ANDROID_EMPTY_ALLOW_CREDENTIALS: |
| return DOMException::Create(DOMExceptionCode::kNotSupportedError, |
| "Use of an empty `allowCredentials` list is " |
| "not supported on this device."); |
| case CredentialManagerError::ANDROID_NOT_SUPPORTED_ERROR: |
| return DOMException::Create(DOMExceptionCode::kNotSupportedError, |
| "Either the device has received unexpected " |
| "request parameters, or the device " |
| "cannot support this request."); |
| case CredentialManagerError::ANDROID_USER_VERIFICATION_UNSUPPORTED: |
| return DOMException::Create(DOMExceptionCode::kNotSupportedError, |
| "The specified `userVerification` " |
| "requirement cannot be fulfilled by " |
| "this device unless the device is secured " |
| "with a screen lock."); |
| case CredentialManagerError::UNKNOWN: |
| return DOMException::Create(DOMExceptionCode::kNotReadableError, |
| "An unknown error occurred while talking " |
| "to the credential manager."); |
| case CredentialManagerError::SUCCESS: |
| NOTREACHED(); |
| break; |
| } |
| return nullptr; |
| } |
| |
| void OnStoreComplete(std::unique_ptr<ScopedPromiseResolver> scoped_resolver, |
| RequiredOriginType required_origin_type) { |
| auto* resolver = scoped_resolver->Release(); |
| AssertSecurityRequirementsBeforeResponse(resolver, required_origin_type); |
| resolver->Resolve(); |
| } |
| |
| void OnPreventSilentAccessComplete( |
| std::unique_ptr<ScopedPromiseResolver> scoped_resolver) { |
| auto* resolver = scoped_resolver->Release(); |
| const auto required_origin_type = RequiredOriginType::kSecure; |
| AssertSecurityRequirementsBeforeResponse(resolver, required_origin_type); |
| |
| resolver->Resolve(); |
| } |
| |
| void OnGetComplete(std::unique_ptr<ScopedPromiseResolver> scoped_resolver, |
| RequiredOriginType required_origin_type, |
| CredentialManagerError error, |
| CredentialInfoPtr credential_info) { |
| auto* resolver = scoped_resolver->Release(); |
| |
| AssertSecurityRequirementsBeforeResponse(resolver, required_origin_type); |
| if (error == CredentialManagerError::SUCCESS) { |
| DCHECK(credential_info); |
| UseCounter::Count(resolver->GetExecutionContext(), |
| WebFeature::kCredentialManagerGetReturnedCredential); |
| resolver->Resolve(mojo::ConvertTo<Credential*>(std::move(credential_info))); |
| } else { |
| DCHECK(!credential_info); |
| resolver->Reject(CredentialManagerErrorToDOMException(error)); |
| } |
| } |
| |
| DOMArrayBuffer* VectorToDOMArrayBuffer(const Vector<uint8_t> buffer) { |
| return DOMArrayBuffer::Create(static_cast<const void*>(buffer.data()), |
| buffer.size()); |
| } |
| |
| void OnMakePublicKeyCredentialComplete( |
| std::unique_ptr<ScopedPromiseResolver> scoped_resolver, |
| AuthenticatorStatus status, |
| MakeCredentialAuthenticatorResponsePtr credential) { |
| auto* resolver = scoped_resolver->Release(); |
| const auto required_origin_type = RequiredOriginType::kSecure; |
| |
| // TODO(crbug.com/803080): Introduce the assert counterpart of |
| // CheckPublicKeySecurityRequirements(). |
| AssertSecurityRequirementsBeforeResponse(resolver, required_origin_type); |
| if (status == AuthenticatorStatus::SUCCESS) { |
| DCHECK(credential); |
| DCHECK(!credential->info->client_data_json.IsEmpty()); |
| DCHECK(!credential->attestation_object.IsEmpty()); |
| 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)); |
| AuthenticatorAttestationResponse* authenticator_response = |
| AuthenticatorAttestationResponse::Create( |
| client_data_buffer, attestation_buffer, credential->transports); |
| |
| AuthenticationExtensionsClientOutputs* extension_outputs = |
| AuthenticationExtensionsClientOutputs::Create(); |
| if (credential->echo_hmac_create_secret) { |
| extension_outputs->setHmacCreateSecret(credential->hmac_create_secret); |
| } |
| resolver->Resolve(PublicKeyCredential::Create(credential->info->id, raw_id, |
| authenticator_response, |
| extension_outputs)); |
| } else { |
| DCHECK(!credential); |
| resolver->Reject(CredentialManagerErrorToDOMException( |
| mojo::ConvertTo<CredentialManagerError>(status))); |
| } |
| } |
| |
| void OnGetAssertionComplete( |
| std::unique_ptr<ScopedPromiseResolver> scoped_resolver, |
| AuthenticatorStatus status, |
| GetAssertionAuthenticatorResponsePtr credential) { |
| auto* resolver = scoped_resolver->Release(); |
| const auto required_origin_type = RequiredOriginType::kSecure; |
| |
| AssertSecurityRequirementsBeforeResponse(resolver, required_origin_type); |
| if (status == AuthenticatorStatus::SUCCESS) { |
| DCHECK(credential); |
| DCHECK(!credential->signature.IsEmpty()); |
| DCHECK(!credential->authenticator_data.IsEmpty()); |
| UseCounter::Count( |
| resolver->GetExecutionContext(), |
| WebFeature::kCredentialManagerGetPublicKeyCredentialSuccess); |
| DOMArrayBuffer* client_data_buffer = |
| VectorToDOMArrayBuffer(std::move(credential->info->client_data_json)); |
| DOMArrayBuffer* raw_id = |
| VectorToDOMArrayBuffer(std::move(credential->info->raw_id)); |
| |
| DOMArrayBuffer* authenticator_buffer = |
| VectorToDOMArrayBuffer(std::move(credential->authenticator_data)); |
| DOMArrayBuffer* signature_buffer = |
| VectorToDOMArrayBuffer(std::move(credential->signature)); |
| DOMArrayBuffer* user_handle = |
| credential->user_handle |
| ? VectorToDOMArrayBuffer(std::move(*credential->user_handle)) |
| : nullptr; |
| AuthenticatorAssertionResponse* authenticator_response = |
| AuthenticatorAssertionResponse::Create(client_data_buffer, |
| authenticator_buffer, |
| signature_buffer, user_handle); |
| AuthenticationExtensionsClientOutputs* extension_outputs = |
| AuthenticationExtensionsClientOutputs::Create(); |
| if (credential->echo_appid_extension) { |
| extension_outputs->setAppid(credential->appid_extension); |
| } |
| resolver->Resolve(PublicKeyCredential::Create(credential->info->id, raw_id, |
| authenticator_response, |
| extension_outputs)); |
| } else { |
| DCHECK(!credential); |
| resolver->Reject(CredentialManagerErrorToDOMException( |
| mojo::ConvertTo<CredentialManagerError>(status))); |
| } |
| } |
| |
| } // namespace |
| |
| CredentialsContainer* CredentialsContainer::Create() { |
| return MakeGarbageCollected<CredentialsContainer>(); |
| } |
| |
| CredentialsContainer::CredentialsContainer() = default; |
| |
| ScriptPromise CredentialsContainer::get( |
| ScriptState* script_state, |
| const CredentialRequestOptions* options) { |
| ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| |
| auto required_origin_type = RequiredOriginType::kSecureAndSameWithAncestors; |
| if (!CheckSecurityRequirementsBeforeRequest(resolver, required_origin_type)) |
| return promise; |
| |
| if (options->hasPublicKey()) { |
| UseCounter::Count(resolver->GetExecutionContext(), |
| WebFeature::kCredentialManagerGetPublicKeyCredential); |
| |
| const String& relying_party_id = options->publicKey()->rpId(); |
| if (!CheckPublicKeySecurityRequirements(resolver, relying_party_id)) |
| 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( |
| DOMException::Create(DOMExceptionCode::kSyntaxError, |
| "The `appid` extension value is neither " |
| "empty/null nor a valid URL")); |
| return promise; |
| } |
| } |
| } |
| if (options->publicKey()->extensions()->hasCableRegistration()) { |
| resolver->Reject(DOMException::Create( |
| DOMExceptionCode::kNotSupportedError, |
| "The 'cableRegistration' extension is only valid when creating " |
| "a credential")); |
| return promise; |
| } |
| } |
| |
| auto mojo_options = |
| MojoPublicKeyCredentialRequestOptions::From(options->publicKey()); |
| if (mojo_options) { |
| if (!mojo_options->relying_party_id) { |
| mojo_options->relying_party_id = resolver->GetFrame() |
| ->GetSecurityContext() |
| ->GetSecurityOrigin() |
| ->Domain(); |
| } |
| auto* authenticator = |
| CredentialManagerProxy::From(script_state)->Authenticator(); |
| authenticator->GetAssertion( |
| std::move(mojo_options), |
| WTF::Bind( |
| &OnGetAssertionComplete, |
| WTF::Passed(std::make_unique<ScopedPromiseResolver>(resolver)))); |
| } else { |
| resolver->Reject(DOMException::Create( |
| DOMExceptionCode::kNotSupportedError, |
| "Required parameters missing in 'options.publicKey'.")); |
| } |
| return promise; |
| } |
| |
| Vector<KURL> providers; |
| if (options->hasFederated() && options->federated()->hasProviders()) { |
| for (const auto& string : options->federated()->providers()) { |
| KURL url = KURL(NullURL(), string); |
| if (url.IsValid()) |
| providers.push_back(std::move(url)); |
| } |
| } |
| |
| CredentialMediationRequirement requirement; |
| if (options->mediation() == "silent") { |
| UseCounter::Count(ExecutionContext::From(script_state), |
| WebFeature::kCredentialManagerGetMediationSilent); |
| requirement = CredentialMediationRequirement::kSilent; |
| } else if (options->mediation() == "optional") { |
| UseCounter::Count(ExecutionContext::From(script_state), |
| WebFeature::kCredentialManagerGetMediationOptional); |
| requirement = CredentialMediationRequirement::kOptional; |
| } else { |
| DCHECK_EQ("required", options->mediation()); |
| UseCounter::Count(ExecutionContext::From(script_state), |
| WebFeature::kCredentialManagerGetMediationRequired); |
| requirement = CredentialMediationRequirement::kRequired; |
| } |
| |
| auto* credential_manager = |
| CredentialManagerProxy::From(script_state)->CredentialManager(); |
| credential_manager->Get( |
| requirement, options->password(), std::move(providers), |
| WTF::Bind(&OnGetComplete, |
| WTF::Passed(std::make_unique<ScopedPromiseResolver>(resolver)), |
| required_origin_type)); |
| |
| return promise; |
| } |
| |
| ScriptPromise CredentialsContainer::store(ScriptState* script_state, |
| Credential* credential) { |
| ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| |
| auto required_origin_type = |
| credential->IsFederatedCredential() || credential->IsPasswordCredential() |
| ? RequiredOriginType::kSecureAndSameWithAncestors |
| : RequiredOriginType::kSecure; |
| if (!CheckSecurityRequirementsBeforeRequest(resolver, required_origin_type)) |
| return promise; |
| |
| if (!(credential->IsFederatedCredential() || |
| credential->IsPasswordCredential())) { |
| resolver->Reject(DOMException::Create( |
| DOMExceptionCode::kNotSupportedError, |
| "Store operation not permitted for PublicKey credentials.")); |
| } |
| |
| if (!IsIconURLEmptyOrSecure(credential)) { |
| resolver->Reject(DOMException::Create(DOMExceptionCode::kSecurityError, |
| "'iconURL' should be a secure URL")); |
| return promise; |
| } |
| |
| auto* credential_manager = |
| CredentialManagerProxy::From(script_state)->CredentialManager(); |
| credential_manager->Store( |
| CredentialInfo::From(credential), |
| WTF::Bind(&OnStoreComplete, |
| WTF::Passed(std::make_unique<ScopedPromiseResolver>(resolver)), |
| required_origin_type)); |
| |
| return promise; |
| } |
| |
| ScriptPromise CredentialsContainer::create( |
| ScriptState* script_state, |
| const CredentialCreationOptions* options, |
| ExceptionState& exception_state) { |
| ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| |
| auto 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(DOMException::Create( |
| DOMExceptionCode::kNotSupportedError, |
| "Only exactly one of 'password', 'federated', and 'publicKey' " |
| "credential types are currently supported.")); |
| return promise; |
| } |
| |
| if (options->hasPassword()) { |
| resolver->Resolve( |
| options->password().IsPasswordCredentialData() |
| ? PasswordCredential::Create( |
| options->password().GetAsPasswordCredentialData(), |
| exception_state) |
| : PasswordCredential::Create( |
| options->password().GetAsHTMLFormElement(), exception_state)); |
| } else if (options->hasFederated()) { |
| resolver->Resolve( |
| FederatedCredential::Create(options->federated(), exception_state)); |
| } else { |
| DCHECK(options->hasPublicKey()); |
| UseCounter::Count(resolver->GetExecutionContext(), |
| WebFeature::kCredentialManagerCreatePublicKeyCredential); |
| |
| const String& relying_party_id = options->publicKey()->rp()->id(); |
| if (!CheckPublicKeySecurityRequirements(resolver, relying_party_id)) |
| return promise; |
| |
| if (options->publicKey()->hasExtensions()) { |
| if (options->publicKey()->extensions()->hasAppid()) { |
| resolver->Reject(DOMException::Create( |
| 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()->hasCableAuthentication()) { |
| resolver->Reject(DOMException::Create( |
| DOMExceptionCode::kNotSupportedError, |
| "The 'cableAuthentication' extension is only valid when requesting " |
| "an assertion")); |
| return promise; |
| } |
| } |
| |
| auto mojo_options = |
| MojoPublicKeyCredentialCreationOptions::From(options->publicKey()); |
| if (!mojo_options) { |
| resolver->Reject(DOMException::Create( |
| DOMExceptionCode::kNotSupportedError, |
| "Required parameters missing in `options.publicKey`.")); |
| } else if (mojo_options->user->id.size() > 64) { |
| // https://www.w3.org/TR/webauthn/#user-handle |
| v8::Isolate* isolate = resolver->GetScriptState()->GetIsolate(); |
| resolver->Reject(V8ThrowException::CreateTypeError( |
| isolate, "User handle exceeds 64 bytes.")); |
| } else { |
| if (!mojo_options->relying_party->id) { |
| mojo_options->relying_party->id = resolver->GetFrame() |
| ->GetSecurityContext() |
| ->GetSecurityOrigin() |
| ->Domain(); |
| } |
| auto* authenticator = |
| CredentialManagerProxy::From(script_state)->Authenticator(); |
| authenticator->MakeCredential( |
| std::move(mojo_options), |
| WTF::Bind( |
| &OnMakePublicKeyCredentialComplete, |
| WTF::Passed(std::make_unique<ScopedPromiseResolver>(resolver)))); |
| } |
| } |
| |
| return promise; |
| } |
| |
| ScriptPromise CredentialsContainer::preventSilentAccess( |
| ScriptState* script_state) { |
| ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| const auto required_origin_type = RequiredOriginType::kSecure; |
| if (!CheckSecurityRequirementsBeforeRequest(resolver, required_origin_type)) |
| return promise; |
| |
| auto* credential_manager = |
| CredentialManagerProxy::From(script_state)->CredentialManager(); |
| credential_manager->PreventSilentAccess(WTF::Bind( |
| &OnPreventSilentAccessComplete, |
| WTF::Passed(std::make_unique<ScopedPromiseResolver>(resolver)))); |
| |
| return promise; |
| } |
| |
| } // namespace blink |