blob: 461267399f0683a16bbe67a0d80ee3c4cfd3788c [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/credentialmanagement/digital_identity_credential.h"
#include <memory>
#include <optional>
#include <utility>
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-forward.h"
#include "third_party/blink/public/mojom/webid/digital_identity_request.mojom-shared.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/public/platform/web_v8_value_converter.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/script_value.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_credential_creation_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_credential_request_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_digital_credential_create_request.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_digital_credential_creation_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_digital_credential_get_request.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_digital_credential_request_options.h"
#include "third_party/blink/renderer/core/dom/abort_signal.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/dom/scoped_abort_state.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/modules/credentialmanagement/credential.h"
#include "third_party/blink/renderer/modules/credentialmanagement/credential_manager_proxy.h"
#include "third_party/blink/renderer/modules/credentialmanagement/credential_manager_type_converters.h" // IWYU pragma: keep
#include "third_party/blink/renderer/modules/credentialmanagement/credential_utils.h"
#include "third_party/blink/renderer/modules/credentialmanagement/digital_credential.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"
namespace blink {
namespace {
using mojom::blink::RequestDigitalIdentityStatus;
enum class DigitalIdentityRequestType {
kGet,
kCreate,
};
// Abort an ongoing WebIdentityDigitalCredential request. This will only be
// called before the request finishes due to `scoped_abort_state`.
void AbortRequest(ScriptState* script_state) {
if (!script_state->ContextIsValid()) {
return;
}
CredentialManagerProxy::From(script_state)->DigitalIdentityRequest()->Abort();
}
// Converts a base::Value to a ScriptObject.
// Returns an empty ScriptObject on failure.
ScriptObject ValueToScriptObject(ScriptState* script_state,
std::optional<base::Value> response) {
if (!response.has_value()) {
return ScriptObject();
}
std::unique_ptr<WebV8ValueConverter> converter =
Platform::Current()->CreateWebV8ValueConverter();
ScriptState::Scope script_state_scope(script_state);
v8::Local<v8::Value> v8_response =
converter->ToV8Value(response.value(), script_state->GetContext());
if (v8_response.IsEmpty() || !v8_response->IsObject()) {
// Parsed value is not an object.
return ScriptObject();
}
return ScriptObject(script_state->GetIsolate(), v8_response);
}
void OnCompleteRequest(ScriptPromiseResolver<IDLNullable<Credential>>* resolver,
std::unique_ptr<ScopedAbortState> scoped_abort_state,
DigitalIdentityRequestType request_type,
RequestDigitalIdentityStatus status,
const String& protocol,
std::optional<base::Value> token) {
switch (status) {
case RequestDigitalIdentityStatus::kErrorTooManyRequests: {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"Only one navigator.credentials.get/create request may be "
"outstanding at one time."));
return;
}
case RequestDigitalIdentityStatus::kErrorCanceled: {
AbortSignal* signal =
scoped_abort_state ? scoped_abort_state->Signal() : nullptr;
if (signal && signal->aborted()) {
auto* script_state = resolver->GetScriptState();
ScriptState::Scope script_state_scope(script_state);
resolver->Reject(signal->reason(script_state));
} else {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kAbortError, "The request has been aborted."));
}
return;
}
case RequestDigitalIdentityStatus::kErrorNoRequests:
resolver->RejectWithTypeError(
"Digital identity API needs at least one request.");
return;
case RequestDigitalIdentityStatus::kErrorInvalidJson:
resolver->RejectWithTypeError(
"Digital identity API requires valid JSON in the request.");
return;
case RequestDigitalIdentityStatus::kErrorNoTransientUserActivation:
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
String::Format("The '%s' feature requires transient activation.",
request_type == DigitalIdentityRequestType::kCreate
? "digital-credentials-create"
: "digital-credentials-get")));
return;
case RequestDigitalIdentityStatus::kError: {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNetworkError, "Error retrieving a token."));
return;
}
case RequestDigitalIdentityStatus::kSuccess: {
switch (request_type) {
case DigitalIdentityRequestType::kGet:
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kIdentityDigitalCredentialsSuccess);
break;
case DigitalIdentityRequestType::kCreate:
UseCounter::Count(
resolver->GetExecutionContext(),
WebFeature::kIdentityDigitalCredentialsCreationSuccess);
break;
}
DigitalCredential* credential = DigitalCredential::Create(
protocol,
ValueToScriptObject(resolver->GetScriptState(), std::move(token)));
resolver->Resolve(credential);
return;
}
}
}
bool IsSerializable(ScriptState* script_state, ScriptObject data) {
v8::Local<v8::String> data_string;
v8::TryCatch try_catch(script_state->GetIsolate());
return v8::JSON::Stringify(script_state->GetContext(), data.V8Object())
.ToLocal(&data_string) &&
!try_catch.HasCaught();
}
} // anonymous namespace
bool IsDigitalIdentityCredentialType(const CredentialRequestOptions& options) {
return options.hasDigital();
}
bool IsDigitalIdentityCredentialType(const CredentialCreationOptions& options) {
return options.hasDigital();
}
void DiscoverDigitalIdentityCredentialFromExternalSource(
ScriptPromiseResolver<IDLNullable<Credential>>* resolver,
const CredentialRequestOptions& options) {
CHECK(IsDigitalIdentityCredentialType(options));
CHECK(RuntimeEnabledFeatures::WebIdentityDigitalCredentialsEnabled(
resolver->GetExecutionContext()));
if (!CheckGenericSecurityRequirementsForCredentialsContainerRequest(
resolver)) {
return;
}
if (!resolver->GetExecutionContext()->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::kDigitalCredentialsGet)) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"The 'digital-credentials-get' feature is not enabled in this "
"document. Permissions Policy may be used to delegate digital "
"credential API capabilities to cross-origin child frames."));
return;
}
std::unique_ptr<WebV8ValueConverter> converter =
Platform::Current()->CreateWebV8ValueConverter();
Vector<blink::mojom::blink::DigitalCredentialGetRequestPtr> requests;
ScriptState* script_state = resolver->GetScriptState();
for (const auto& request : options.digital()->requests()) {
if (!IsSerializable(script_state, request->data())) {
resolver->RejectWithTypeError(
"Digital identity API requires valid JSON in the request.");
return;
}
blink::mojom::blink::DigitalCredentialGetRequestPtr
digital_credential_request =
blink::mojom::blink::DigitalCredentialGetRequest::New();
digital_credential_request->protocol = request->protocol();
std::unique_ptr<base::Value> digital_credential_request_data =
converter->FromV8Value(request->data().V8Object(),
resolver->GetScriptState()->GetContext());
if (!digital_credential_request_data) {
return;
}
digital_credential_request->data =
std::move(*digital_credential_request_data);
requests.push_back(std::move(digital_credential_request));
}
if (requests.empty()) {
resolver->RejectWithTypeError(
"Digital identity API needs at least one well-formed request.");
return;
}
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kIdentityDigitalCredentials);
std::unique_ptr<ScopedAbortState> scoped_abort_state;
if (auto* signal = options.getSignalOr(nullptr)) {
auto callback = BindOnce(&AbortRequest, WrapPersistent(script_state));
auto* handle = signal->AddAlgorithm(std::move(callback));
scoped_abort_state = std::make_unique<ScopedAbortState>(signal, handle);
}
auto* request =
CredentialManagerProxy::From(script_state)->DigitalIdentityRequest();
request->Get(std::move(requests),
blink::BindOnce(&OnCompleteRequest, WrapPersistent(resolver),
std::move(scoped_abort_state),
DigitalIdentityRequestType::kGet));
}
void CreateDigitalIdentityCredentialInExternalSource(
ScriptPromiseResolver<IDLNullable<Credential>>* resolver,
const CredentialCreationOptions& options) {
CHECK(IsDigitalIdentityCredentialType(options));
CHECK(RuntimeEnabledFeatures::WebIdentityDigitalCredentialsCreationEnabled(
resolver->GetExecutionContext()));
if (!CheckGenericSecurityRequirementsForCredentialsContainerRequest(
resolver)) {
return;
}
if (!resolver->GetExecutionContext()->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::kDigitalCredentialsCreate)) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"The 'digital-credentials-create' feature is not enabled in this "
"document. Permissions Policy may be used to delegate digital "
"credential API capabilities to cross-origin child frames."));
return;
}
std::unique_ptr<WebV8ValueConverter> converter =
Platform::Current()->CreateWebV8ValueConverter();
Vector<blink::mojom::blink::DigitalCredentialCreateRequestPtr> requests;
ScriptState* script_state = resolver->GetScriptState();
for (const auto& request : options.digital()->requests()) {
if (!IsSerializable(script_state, request->data())) {
resolver->RejectWithTypeError(
"Digital identity API requires valid JSON in the request.");
return;
}
blink::mojom::blink::DigitalCredentialCreateRequestPtr
digital_credential_request =
blink::mojom::blink::DigitalCredentialCreateRequest::New();
digital_credential_request->protocol = request->protocol();
std::unique_ptr<base::Value> digital_credential_request_data =
converter->FromV8Value(request->data().V8Object(),
resolver->GetScriptState()->GetContext());
if (!digital_credential_request_data) {
continue;
}
digital_credential_request->data =
std::move(*digital_credential_request_data);
requests.push_back(std::move(digital_credential_request));
}
if (requests.empty()) {
resolver->RejectWithTypeError(
"Digital identity API needs at least one well-formed request.");
return;
}
UseCounter::Count(resolver->GetExecutionContext(),
WebFeature::kIdentityDigitalCredentialsCreation);
std::unique_ptr<ScopedAbortState> scoped_abort_state;
if (auto* signal = options.getSignalOr(nullptr)) {
auto callback = BindOnce(&AbortRequest, WrapPersistent(script_state));
auto* handle = signal->AddAlgorithm(std::move(callback));
scoped_abort_state = std::make_unique<ScopedAbortState>(signal, handle);
}
CredentialManagerProxy::From(script_state)
->DigitalIdentityRequest()
->Create(std::move(requests),
blink::BindOnce(&OnCompleteRequest, WrapPersistent(resolver),
std::move(scoped_abort_state),
DigitalIdentityRequestType::kCreate));
}
} // namespace blink