This commit is contained in:
devgianlu 2025-04-19 11:18:51 +00:00 committed by GitHub
commit 3d0395d666
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 899 additions and 26 deletions

View file

@ -12,15 +12,15 @@ namespace Web::CredentialManagement {
// https://www.w3.org/TR/credential-management-1/#dom-credential-isconditionalmediationavailable
GC::Ref<WebIDL::Promise> Credential::is_conditional_mediation_available(JS::VM& vm)
{
auto* realm = vm.current_realm();
return WebIDL::create_rejected_promise_from_exception(*realm, vm.throw_completion<JS::InternalError>(JS::ErrorType::NotImplemented, "is conditional mediation available"sv));
// 1. Return a promise resolved with false.
return WebIDL::create_resolved_promise(*vm.current_realm(), JS::Value(false));
}
// https://www.w3.org/TR/credential-management-1/#dom-credential-willrequestconditionalcreation
GC::Ref<WebIDL::Promise> Credential::will_request_conditional_creation(JS::VM& vm)
{
auto* realm = vm.current_realm();
return WebIDL::create_rejected_promise_from_exception(*realm, vm.throw_completion<JS::InternalError>(JS::ErrorType::NotImplemented, "will request conditional creation"sv));
// 1. Return a promise resolved with undefined.
return WebIDL::create_resolved_promise(*vm.current_realm(), JS::js_undefined());
}
Credential::~Credential() { }

View file

@ -13,6 +13,74 @@
namespace Web::CredentialManagement {
typedef GC::Function<JS::ThrowCompletionOr<GC::Ref<Credential>>(JS::Object const&)> CreateCredentialAlgorithm;
#define CREDENTIAL_INTERFACE(class_name) \
public: \
static class_name const* the() \
{ \
static class_name* instance = nullptr; \
if (!instance) \
instance = new class_name(); \
return instance; \
}
class CredentialInterface {
AK_MAKE_NONCOPYABLE(CredentialInterface);
protected:
CredentialInterface() { }
public:
virtual ~CredentialInterface() = default;
// https://w3c.github.io/webappsec-credential-management/#credential-type-registry-credential-type
virtual String type() const = 0;
// https://w3c.github.io/webappsec-credential-management/#credential-type-registry-options-member-identifier
virtual String options_member_identifier() const = 0;
// https://w3c.github.io/webappsec-credential-management/#credential-type-registry-get-permissions-policy
virtual Optional<String> get_permission_policy() const = 0;
// https://w3c.github.io/webappsec-credential-management/#credential-type-registry-create-permissions-policy
virtual Optional<String> create_permission_policy() const = 0;
// https://w3c.github.io/webappsec-credential-management/#dom-credential-discovery-slot
virtual String discovery() const = 0;
// NOTE: This is not explicitly present in the spec, it is inferred.
virtual bool supports_conditional_user_mediation() const = 0;
// https://w3c.github.io/webappsec-credential-management/#algorithm-create-cred
virtual JS::ThrowCompletionOr<Variant<Empty, GC::Ref<Credential>, GC::Ref<CreateCredentialAlgorithm>>> create(JS::Realm&, URL::Origin const&, CredentialCreationOptions const&, bool) const
{
// 1. Return null.
return Empty {};
}
// https://w3c.github.io/webappsec-credential-management/#algorithm-store-cred
virtual JS::ThrowCompletionOr<void> store(JS::Realm& realm, bool) const
{
// 1. Throw a NotSupportedError.
return throw_completion(WebIDL::NotSupportedError::create(realm, "store"_string));
}
// https://w3c.github.io/webappsec-credential-management/#algorithm-discover-creds
virtual JS::ThrowCompletionOr<Variant<Empty, GC::Ref<Credential>>> discover_from_external_source(JS::Realm&, URL::Origin const&, CredentialRequestOptions const&, bool) const
{
// 1. Return null.
return Empty {};
}
// https://w3c.github.io/webappsec-credential-management/#algorithm-collect-creds
virtual JS::ThrowCompletionOr<Vector<Credential>> collect_from_credential_store(JS::Realm&, URL::Origin const&, CredentialRequestOptions const&, bool) const
{
// 1. Return an empty set.
return Vector<Credential> {};
}
};
class Credential : public Bindings::PlatformObject {
WEB_PLATFORM_OBJECT(Credential, Bindings::PlatformObject);
GC_DECLARE_ALLOCATOR(Credential);
@ -25,11 +93,12 @@ public:
virtual ~Credential() override;
String const& id() { return m_id; }
String const& name() { return m_name; }
String const& icon_url() { return m_icon_url; }
String const& id() const { return m_id; }
String const& name() const { return m_name; }
String const& icon_url() const { return m_icon_url; }
virtual String type() = 0;
virtual String type() const = 0;
virtual CredentialInterface const* interface() const = 0;
protected:
explicit Credential(JS::Realm&);

View file

@ -5,6 +5,9 @@
*/
#include <LibWeb/CredentialManagement/CredentialsContainer.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
namespace Web::CredentialManagement {
@ -17,25 +20,487 @@ GC::Ref<CredentialsContainer> CredentialsContainer::create(JS::Realm& realm)
CredentialsContainer::~CredentialsContainer() { }
// https://www.w3.org/TR/credential-management-1/#dom-credentialscontainer-get
GC::Ref<WebIDL::Promise> CredentialsContainer::get(CredentialRequestOptions const&)
// https://w3c.github.io/webappsec-credential-management/#algorithm-same-origin-with-ancestors
static bool is_same_origin_with_its_ancestors(HTML::EnvironmentSettingsObject& settings)
{
auto* realm = vm().current_realm();
return WebIDL::create_rejected_promise_from_exception(*realm, vm().throw_completion<JS::InternalError>(JS::ErrorType::NotImplemented, "get"sv));
auto& global = settings.global_object();
// TODO: 1. If settingss relevant global object has no associated Document, return false.
// 2. Let document be settings relevant global object's associated Document.
auto& document = as<HTML::Window>(global).associated_document();
// 3. If document has no browsing context, return false.
if (!document.browsing_context())
return false;
// 4. Let origin be settings origin.
auto origin = settings.origin();
// 5. Let navigable be documents node navigable.
auto navigable = document.navigable();
// 6. While navigable has a non-null parent:
while (navigable->parent()) {
// 1. Set navigable to navigables parent.
navigable = navigable->parent();
// 2. If navigables active document's origin is not same origin with origin, return false.
if (!origin.is_same_origin(navigable->active_document()->origin()))
return false;
}
// 7. Return true.
return true;
}
// https://www.w3.org/TR/credential-management-1/#dom-credentialscontainer-store
GC::Ref<WebIDL::Promise> CredentialsContainer::store(Credential const&)
// https://w3c.github.io/webappsec-credential-management/#credentialrequestoptions-relevant-credential-interface-objects
template<typename OptionsType>
static Vector<CredentialInterface const*> relevant_credential_interface_objects(OptionsType const& options)
{
auto* realm = vm().current_realm();
return WebIDL::create_rejected_promise_from_exception(*realm, vm().throw_completion<JS::InternalError>(JS::ErrorType::NotImplemented, "store"sv));
// 1. Let settings be the current settings object.
// 2. Let relevant interface objects be an empty set.
Vector<CredentialInterface const*> interfaces;
// 3. For each optionKey → optionValue of options:
// NOTE: We cannot iterate like the spec says.
// 1. Let credentialInterfaceObject be the Appropriate Interface Object (on settings global object) whose Options Member Identifier is optionKey.
// 2. Assert: credentialInterfaceObjects [[type]] slot equals the Credential Type whose Options Member Identifier is optionKey.
// 3. Append credentialInterfaceObject to relevant interface objects.
#define APPEND_CREDENTIAL_INTERFACE_OBJECT(key, type_) \
if (options.key.has_value()) { \
auto credential_interface_object = type_##Interface::the(); \
VERIFY(credential_interface_object->options_member_identifier() == #key); \
interfaces.append(credential_interface_object); \
}
// https://w3c.github.io/webappsec-credential-management/#credential-type-registry-appropriate-interface-object
APPEND_CREDENTIAL_INTERFACE_OBJECT(password, PasswordCredential);
APPEND_CREDENTIAL_INTERFACE_OBJECT(federated, FederatedCredential);
// TODO: digital
// TODO: identity
// TODO: otp
// TODO: publicKey
#undef APPEND_CREDENTIAL_INTERFACE_OBJECT
// 4. Return relevant interface objects.
return interfaces;
}
// https://www.w3.org/TR/credential-management-1/#dom-credentialscontainer-create
GC::Ref<WebIDL::Promise> CredentialsContainer::create(CredentialCreationOptions const&)
// https://w3c.github.io/webappsec-credential-management/#algorithm-collect-known
static JS::ThrowCompletionOr<Vector<GC::Ref<Credential>>> collect_credentials_from_store(JS::Realm& realm, URL::Origin const& origin, CredentialRequestOptions const& options, bool same_origin_with_ancestors)
{
auto* realm = vm().current_realm();
return WebIDL::create_rejected_promise_from_exception(*realm, vm().throw_completion<JS::InternalError>(JS::ErrorType::NotImplemented, "create"sv));
// 1. Let possible matches be an empty set.
Vector<GC::Ref<Credential>> possible_matches;
// 2. For each interface in options relevant credential interface objects:
for (auto& interface : relevant_credential_interface_objects(options)) {
// 1. Let r be the result of executing interfaces [[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
// internal method on origin, options, and sameOriginWithAncestors. If that threw an exception, rethrow that exception.
auto maybe_r = interface->collect_from_credential_store(realm, origin, options, same_origin_with_ancestors);
if (maybe_r.is_error())
return maybe_r.error();
auto r = maybe_r.release_value();
// TODO: 2. Assert: r is a list of interface objects.
// 3. For each c in r:
for (auto& c : r) {
// 1. Append c to possible matches.
possible_matches.append(c);
}
}
// 3. Return possible matches.
return possible_matches;
}
// https://w3c.github.io/webappsec-credential-management/#abstract-opdef-ask-the-user-to-choose-a-credential
static Variant<Empty, GC::Ref<Credential>, CredentialInterface*> ask_the_user_to_choose_a_credential(CredentialRequestOptions const&, Vector<GC::Ref<Credential>> const&)
{
// TODO: This algorithm returns either null if the user chose not to share a credential with the site,
// a Credential object if the user chose a specific credential, or a Credential interface object
// if the user chose a type of credential.
return {};
}
// https://w3c.github.io/webappsec-credential-management/#credentialrequestoptions-matchable-a-priori
static bool is_matchable_a_priori(CredentialRequestOptions const& options)
{
// 1. For each interface in options relevant credential interface objects:
for (auto& interface : relevant_credential_interface_objects(options)) {
// 1. If interfaces [[discovery]] slots value is not "credential store", return false.
if (interface->discovery() != "credential store")
return false;
}
// 2. Return true.
return true;
}
// https://w3c.github.io/webappsec-credential-management/#algorithm-request
GC::Ref<WebIDL::Promise> CredentialsContainer::get(CredentialRequestOptions const& options)
{
// 1. Let settings be the current settings object.
auto& settings = HTML::current_principal_settings_object();
// 2. Assert: settings is a secure context.
VERIFY(HTML::is_secure_context(settings));
// 3. Let document be settingss relevant global object's associated Document.
auto& document = as<HTML::Window>(settings.global_object()).associated_document();
// 4. If document is not fully active, then return a promise rejected with an "InvalidStateError" DOMException.
if (!document.is_fully_active())
return WebIDL::create_rejected_promise_from_exception(realm(), WebIDL::InvalidStateError::create(realm(), "Document is not fully active"_string));
// 5. If options.signal is aborted, then return a promise rejected with options.signals abort reason.
if (options.signal && options.signal->aborted())
return WebIDL::create_rejected_promise(realm(), options.signal->reason());
// 6. Let interfaces be optionss relevant credential interface objects.
auto interfaces = relevant_credential_interface_objects(options);
// 7. If interfaces is empty, then return a promise rejected with a "NotSupportedError" DOMException.
if (interfaces.is_empty())
return WebIDL::create_rejected_promise_from_exception(realm(), WebIDL::NotSupportedError::create(realm(), "No credential types"_string));
// 8. For each interface of interfaces:
for (auto& interface : interfaces) {
// 1. If options.mediation is conditional and interface does not support conditional user mediation,
// return a promise rejected with a "TypeError" DOMException.
if (options.mediation == Bindings::CredentialMediationRequirement::Conditional && !interface->supports_conditional_user_mediation())
return WebIDL::create_rejected_promise(realm(), JS::TypeError::create(realm(), "Conditional user mediation is not supported"sv));
// 2. If settings active credential types contains interfaces [[type]],
// return a promise rejected with a "NotAllowedError" DOMException.
if (settings.active_credential_types().contains_slow(interface->type()))
return WebIDL::create_rejected_promise_from_exception(realm(), WebIDL::NotAllowedError::create(realm(), "Credential type is not allowed"_string));
// 3. Append interfaces [[type]] to settings active credential types.
settings.active_credential_types().append(interface->type());
}
// 9. Let origin be settings origin.
auto origin = settings.origin();
// 10. Let sameOriginWithAncestors be true if settings is same-origin with its ancestors, and false otherwise.
auto same_origin_with_ancestors = is_same_origin_with_its_ancestors(settings);
// 11. For each interface in options relevant credential interface objects:
for (auto& interface : interfaces) {
// 1. Let permission be the interfaces [[type]] Get Permissions Policy.
auto permission = interface->get_permission_policy();
// 2. If permission is null, continue.
if (!permission.has_value())
continue;
// TODO: 3. If document is not allowed to use permission, return a promise rejected with a "NotAllowedError" DOMException.
}
// 12. Let p be a new promise.
auto promise = WebIDL::create_promise(realm());
// 13. Run the following steps in parallel:
// FIXME: The promises resolving/rejection must be wrapped in a queued task.
// https://github.com/w3c/webappsec-credential-management/issues/260
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm().heap(), [this, promise, origin, options, same_origin_with_ancestors, &settings] {
HTML::TemporaryExecutionContext execution_context { realm(), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// 1. Let credentials be the result of collecting Credentials from the credential store, given origin, options, and sameOriginWithAncestors.
auto maybe_credentials = collect_credentials_from_store(realm(), origin, options, same_origin_with_ancestors);
// 2. If credentials is an exception, reject p with credentials.
if (maybe_credentials.is_error()) {
WebIDL::reject_promise(realm(), *promise, maybe_credentials.error_value());
return;
}
auto credentials = maybe_credentials.release_value();
// 3. If all of the following statements are true, resolve p with credentials[0] and skip the remaining steps:
// 1. credentials size is 1
// TODO: 2. origin does not require user mediation
// 3. options is matchable a priori.
// 4. options.mediation is not "required".
// 5. options.mediation is not "conditional".
if (credentials.size() == 1
&& is_matchable_a_priori(options)
&& options.mediation != Bindings::CredentialMediationRequirement::Required
&& options.mediation != Bindings::CredentialMediationRequirement::Conditional) {
WebIDL::resolve_promise(realm(), *promise, credentials[0]);
return;
}
// 4. If options mediation is "silent", resolve p with null, and skip the remaining steps.
if (options.mediation == Bindings::CredentialMediationRequirement::Silent) {
WebIDL::resolve_promise(realm(), *promise, JS::js_null());
return;
}
// 5. Let result be the result of asking the user to choose a Credential, given options and credentials.
auto result = ask_the_user_to_choose_a_credential(options, credentials);
// 6. If result is an interface object:
if (result.has<CredentialInterface*>()) {
// 1. Set result to the result of executing results [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors),
// given origin, options, and sameOriginWithAncestors.
auto maybe_result = result.get<CredentialInterface*>()->discover_from_external_source(realm(), origin, options, same_origin_with_ancestors);
// If that threw an exception:
if (maybe_result.is_error()) {
// 1. Let e be the thrown exception.
auto e = maybe_result.error_value();
// 2. Queue a task on globals DOM manipulation task source to run the following substeps:
queue_global_task(HTML::Task::Source::DOMManipulation, settings.global_object(), GC::create_function(realm().heap(), [&] {
HTML::TemporaryExecutionContext execution_context { realm(), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// 1. Reject p with e.
WebIDL::reject_promise(realm(), *promise, e);
}));
// 3. Terminate these substeps.
return;
}
}
// 7. Assert: result is null, or a Credential.
VERIFY(result.has<Empty>() || result.has<GC::Ref<Credential>>());
// 8. If result is a Credential, resolve p with result.
if (result.has<GC::Ref<Credential>>()) {
WebIDL::resolve_promise(realm(), *promise, result.get<GC::Ref<Credential>>());
return;
}
// 9. If result is null and options.mediation is not conditional, resolve p with result.
if (result.has<Empty>() && options.mediation != Bindings::CredentialMediationRequirement::Conditional)
WebIDL::resolve_promise(realm(), *promise, JS::js_null());
// FIXME: It is unclear what to do if result is null and options.mediation is conditional.
// https://github.com/w3c/webappsec-credential-management/issues/271
VERIFY_NOT_REACHED();
}));
// 14. React to p:
auto on_completion = GC::create_function(realm().heap(), [&settings, interfaces = move(interfaces)](JS::Value) -> WebIDL::ExceptionOr<JS::Value> {
// 1. For each interface in interfaces:
for (auto const& interface : interfaces) {
// 1. Remove interfaces [[type]] from settings active credential types.
settings.active_credential_types().remove_first_matching([&](auto& v) { return v == interface->type(); });
}
return JS::js_undefined();
});
WebIDL::react_to_promise(*promise, on_completion, on_completion);
// 15. Return p.
return promise;
}
// https://w3c.github.io/webappsec-credential-management/#algorithm-store
GC::Ref<WebIDL::Promise> CredentialsContainer::store(Credential const& credential)
{
// 1. Let settings be the current settings object.
auto& settings = HTML::current_principal_settings_object();
// 2. Assert: settings is a secure context.
VERIFY(HTML::is_secure_context(settings));
// 3. If settingss relevant global object's associated Document is not fully active,
// then return a promise rejected with an "InvalidStateError" DOMException.
if (!as<HTML::Window>(settings.global_object()).associated_document().is_fully_active())
return WebIDL::create_rejected_promise_from_exception(realm(), WebIDL::InvalidStateError::create(realm(), "Document is not fully active"_string));
// 4. Let sameOriginWithAncestors be true if the current settings object is same-origin with its ancestors, and false otherwise.
auto same_origin_with_ancestors = is_same_origin_with_its_ancestors(settings);
// 5. Let p be a new promise.
auto promise = WebIDL::create_promise(realm());
// 6. If settings active credential types contains credentials [[type]], return a promise rejected with a "NotAllowedError" DOMException.
if (settings.active_credential_types().contains_slow(credential.type()))
return WebIDL::create_rejected_promise_from_exception(realm(), WebIDL::NotAllowedError::create(realm(), "Credential type is not allowed"_string));
// 7. Append credentials [[type]] to settings active credential types.
settings.active_credential_types().append(credential.type());
// 8. Run the following steps in parallel:
// FIXME: The promises resolving/rejection must be wrapped in a queued task.
// https://github.com/w3c/webappsec-credential-management/issues/260
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm().heap(), [this, promise, &settings, &credential, same_origin_with_ancestors] {
HTML::TemporaryExecutionContext execution_context { realm(), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// 1. Execute credentials interface object's [[Store]](credential, sameOriginWithAncestors)
// internal method on credential and sameOriginWithAncestors.
auto maybe_error = credential.interface()->store(realm(), same_origin_with_ancestors);
// If that threw an exception:
if (maybe_error.is_error()) {
// 1. Let e be the thrown exception.
auto e = maybe_error.error_value();
// 2. Queue a task on globals DOM manipulation task source to run the following substeps:
queue_global_task(HTML::Task::Source::DOMManipulation, settings.global_object(), GC::create_function(realm().heap(), [&] {
HTML::TemporaryExecutionContext execution_context { realm(), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// 1. Reject p with e.
WebIDL::reject_promise(realm(), *promise, e);
}));
}
// Otherwise, resolve p with undefined.
else {
WebIDL::resolve_promise(realm(), *promise, JS::js_undefined());
}
}));
// 9. React to p:
auto on_completion = GC::create_function(realm().heap(), [&settings, &credential](JS::Value) -> WebIDL::ExceptionOr<JS::Value> {
// 1. Remove credentials [[type]] from settings active credential types.
settings.active_credential_types().remove_first_matching([&](auto& v) { return v == credential.type(); });
return JS::js_undefined();
});
WebIDL::react_to_promise(*promise, on_completion, on_completion);
// 10. Return p.
return promise;
}
// https://w3c.github.io/webappsec-credential-management/#algorithm-create
GC::Ref<WebIDL::Promise> CredentialsContainer::create(CredentialCreationOptions const& options)
{
// 1. Let settings be the current settings object.
auto& settings = HTML::current_principal_settings_object();
// 2. Assert: settings is a secure context.
VERIFY(HTML::is_secure_context(settings));
// 3. Let global be settings global object.
auto& global = settings.global_object();
// 4. Let document be the relevant global object's associated Document.
auto& document = as<HTML::Window>(global).associated_document();
// 5. If document is not fully active, then return a promise rejected with an "InvalidStateError" DOMException.
if (!document.is_fully_active())
return WebIDL::create_rejected_promise_from_exception(realm(), WebIDL::InvalidStateError::create(realm(), "Document is not fully active"_string));
// 6. Let sameOriginWithAncestors be true if the current settings object is same-origin with its ancestors, and false otherwise.
auto same_origin_with_ancestors = is_same_origin_with_its_ancestors(settings);
// 7. Let interfaces be the set of options relevant credential interface objects.
auto interfaces = relevant_credential_interface_objects(options);
// 8. Return a promise rejected with NotSupportedError if any of the following statements are true:
// TODO: 1. global does not have an associated Document.
// 2. interfaces size is greater than 1.
if (interfaces.size() > 1)
return WebIDL::create_rejected_promise_from_exception(realm(), WebIDL::NotSupportedError::create(realm(), "Too many crendetial types"_string));
// 9. For each interface in interfaces:
for (auto& interface : interfaces) {
// 1. Let permission be the interfaces [[type]] Create Permissions Policy.
auto permission = interface->create_permission_policy();
// 2. If permission is null, continue.
if (!permission.has_value())
continue;
// TODO: 3. If document is not allowed to use permission, return a promise rejected with a "NotAllowedError" DOMException.
}
// 10. If options.signal is aborted, then return a promise rejected with options.signals abort reason.
if (options.signal && options.signal->aborted())
return WebIDL::create_rejected_promise(realm(), options.signal->reason());
// NOTE: The spec does not mention this check
// https://github.com/w3c/webappsec-credential-management/issues/267
if (interfaces.size() < 1)
return WebIDL::create_rejected_promise_from_exception(realm(), WebIDL::NotSupportedError::create(realm(), "No credential types"_string));
// 11. Let type be interfaces[0]'s [[type]].
auto type = interfaces[0]->type();
// 12. If settings active credential types contains type, return a promise rejected with a "NotAllowedError" DOMException.
if (settings.active_credential_types().contains_slow(type))
return WebIDL::create_rejected_promise_from_exception(realm(), WebIDL::NotAllowedError::create(realm(), "Credential type is not allowed"_string));
// 13. Append type to settings active credential types.
settings.active_credential_types().append(type);
// 14. Let origin be settingss origin.
auto origin = settings.origin();
// 15. Let p be a new promise.
auto promise = WebIDL::create_promise(realm());
// 16. Run the following steps in parallel:
// FIXME: The promises resolving/rejection must be wrapped in a queued task.
// https://github.com/w3c/webappsec-credential-management/issues/260
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm().heap(), [this, promise, &global, interfaces = move(interfaces), origin, options, same_origin_with_ancestors] {
HTML::TemporaryExecutionContext execution_context { realm(), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// 1. Let r be the result of executing interfaces[0]'s [[Create]](origin, options, sameOriginWithAncestors)
// internal method on origin, options, and sameOriginWithAncestors.
auto maybe_r = interfaces[0]->create(realm(), origin, options, same_origin_with_ancestors);
// If that threw an exception:
if (maybe_r.is_error()) {
// 1. Let e be the thrown exception.
auto e = maybe_r.error_value();
// 2. Queue a task on globals DOM manipulation task source to run the following substeps:
queue_global_task(HTML::Task::Source::DOMManipulation, global, GC::create_function(realm().heap(), [&] {
HTML::TemporaryExecutionContext execution_context { realm(), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// 1. Reject p with e.
WebIDL::reject_promise(realm(), *promise, e);
}));
// 3. Terminate these substeps.
return;
}
auto r = maybe_r.release_value();
// 2. If r is a Credential or null, resolve p with r, and terminate these substeps.
if (r.has<Empty>()) {
WebIDL::resolve_promise(realm(), *promise, JS::js_null());
return;
}
if (r.has<GC::Ref<Credential>>()) {
auto& credential = r.get<GC::Ref<Credential>>();
WebIDL::resolve_promise(realm(), *promise, credential);
return;
}
// 3. Assert: r is an algorithm (as defined in §2.2.1.4 [[Create]] internal method).
VERIFY(r.has<GC::Ref<CreateCredentialAlgorithm>>());
// 4. Queue a task on globals DOM manipulation task source to run the following substeps:
auto& r_algo = r.get<GC::Ref<CreateCredentialAlgorithm>>();
queue_global_task(HTML::Task::Source::DOMManipulation, global, GC::create_function(realm().heap(), [this, &global, promise, r_algo] {
HTML::TemporaryExecutionContext execution_context { realm(), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// 1. Resolve p with the result of promise-calling r given global.
auto maybe_result = r_algo->function()(global);
if (maybe_result.is_error()) {
WebIDL::reject_promise(realm(), *promise, maybe_result.error_value());
return;
}
auto& result = maybe_result.value();
WebIDL::resolve_promise(realm(), *promise, result);
}));
}));
// 17. React to p:
auto on_completion = GC::create_function(realm().heap(), [&settings, type](JS::Value) -> WebIDL::ExceptionOr<JS::Value> {
// 1. Remove type from settings active credential types.
settings.active_credential_types().remove_first_matching([&](auto& v) { return v == type; });
return JS::js_undefined();
});
WebIDL::react_to_promise(*promise, on_completion, on_completion);
// 18. Return p.
return promise;
}
// https://www.w3.org/TR/credential-management-1/#dom-credentialscontainer-preventsilentaccess

View file

@ -13,6 +13,24 @@
namespace Web::CredentialManagement {
class FederatedCredentialInterface final : public CredentialInterface {
CREDENTIAL_INTERFACE(FederatedCredentialInterface);
public:
virtual String type() const override { return "federated"_string; }
virtual String options_member_identifier() const override { return "federated"_string; }
virtual Optional<String> get_permission_policy() const override { return {}; }
virtual Optional<String> create_permission_policy() const override { return {}; }
virtual String discovery() const override { return "credential store"_string; }
virtual bool supports_conditional_user_mediation() const override
{
// NOTE: FederatedCredential does not override is_conditional_mediation_available(),
// therefore conditional mediation is not supported.
return false;
}
};
class FederatedCredential final : public Credential {
WEB_PLATFORM_OBJECT(FederatedCredential, Credential);
GC_DECLARE_ALLOCATOR(FederatedCredential);
@ -23,10 +41,14 @@ public:
virtual ~FederatedCredential() override;
String const& provider() { return m_provider; }
Optional<String> const& protocol() { return m_protocol; }
String const& provider() const { return m_provider; }
Optional<String> const& protocol() const { return m_protocol; }
String type() override { return "federated"_string; }
String type() const override { return "federated"_string; }
virtual CredentialInterface const* interface() const override
{
return FederatedCredentialInterface::the();
}
private:
explicit FederatedCredential(JS::Realm&);

View file

@ -14,6 +14,24 @@
namespace Web::CredentialManagement {
class PasswordCredentialInterface final : public CredentialInterface {
CREDENTIAL_INTERFACE(PasswordCredentialInterface);
public:
virtual String type() const override { return "password"_string; }
virtual String options_member_identifier() const override { return "password"_string; }
virtual Optional<String> get_permission_policy() const override { return {}; }
virtual Optional<String> create_permission_policy() const override { return {}; }
virtual String discovery() const override { return "credential store"_string; }
virtual bool supports_conditional_user_mediation() const override
{
// NOTE: PasswordCredential does not override is_conditional_mediation_available(),
// therefore conditional mediation is not supported.
return false;
}
};
class PasswordCredential final : public Credential {
WEB_PLATFORM_OBJECT(PasswordCredential, Credential);
GC_DECLARE_ALLOCATOR(PasswordCredential);
@ -25,9 +43,13 @@ public:
virtual ~PasswordCredential() override;
String const& password() { return m_password; }
String const& password() const { return m_password; }
String type() override { return "password"_string; }
String type() const override { return "password"_string; }
virtual CredentialInterface const* interface() const override
{
return PasswordCredentialInterface::the();
}
private:
explicit PasswordCredential(JS::Realm&);

View file

@ -9,7 +9,10 @@ interface PasswordCredential : Credential {
PasswordCredential includes CredentialUserData;
partial dictionary CredentialRequestOptions {
boolean password = false;
// FIXME: Spec bug?
// https://github.com/w3c/webappsec-credential-management/issues/270
// boolean password = false;
boolean password;
};
dictionary PasswordCredentialData : CredentialData {

View file

@ -646,4 +646,10 @@ GC::Ref<ServiceWorker::ServiceWorker> EnvironmentSettingsObject::get_service_wor
return *object_map.get(service_worker);
}
// https://w3c.github.io/webappsec-credential-management/#active-credential-types
Vector<String>& EnvironmentSettingsObject::active_credential_types()
{
return m_active_credential_types;
}
}

View file

@ -124,6 +124,9 @@ public:
// https://w3c.github.io/ServiceWorker/#get-the-service-worker-object
GC::Ref<ServiceWorker::ServiceWorker> get_service_worker_object(ServiceWorker::ServiceWorkerRecord*);
// https://w3c.github.io/webappsec-credential-management/#active-credential-types
Vector<String>& active_credential_types();
[[nodiscard]] bool discarded() const { return m_discarded; }
void set_discarded(bool b) { m_discarded = b; }
@ -158,6 +161,10 @@ private:
// a map where the keys are service workers and the values are ServiceWorker objects.
HashMap<ServiceWorker::ServiceWorkerRecord*, GC::Ref<ServiceWorker::ServiceWorker>> m_service_worker_object_map;
// https://w3c.github.io/webappsec-credential-management/#active-credential-types
// Each environment settings object has an associated active credential types, a set which is initially empty.
Vector<String> m_active_credential_types;
// https://w3c.github.io/ServiceWorker/#service-worker-client-discarded-flag
// A service worker client has an associated discarded flag. It is initially unset.
bool m_discarded { false };

View file

@ -0,0 +1,23 @@
Harness status: OK
Found 17 tests
11 Pass
6 Fail
Pass navigator.credentials.create() with no argument.
Pass navigator.credentials.create() with empty argument.
Fail navigator.credentials.create() with valid PasswordCredentialData
Fail navigator.credentials.create() with valid HTMLFormElement
Pass navigator.credentials.create() with bogus password data
Fail navigator.credentials.create() with valid FederatedCredentialData
Pass navigator.credentials.create() with bogus federated data
Fail navigator.credentials.create() with bogus publicKey data
Fail navigator.credentials.create() with both PasswordCredentialData and FederatedCredentialData
Pass navigator.credentials.create() with bogus password and federated data
Pass navigator.credentials.create() with bogus federated and publicKey data
Pass navigator.credentials.create() with bogus password and publicKey data
Pass navigator.credentials.create() with bogus password, federated, and publicKey data
Pass navigator.credentials.create() with bogus data
Pass navigator.credentials.create() aborted with custom reason
Pass navigator.credentials.create() aborted with different objects
Fail navigator.credentials.create() rejects when aborted after the promise creation

View file

@ -0,0 +1,10 @@
Harness status: OK
Found 4 tests
3 Pass
1 Fail
Pass Calling navigator.credentials.get() without a valid matching interface.
Pass navigator.credentials.get() aborted with custom reason
Pass navigator.credentials.get() aborted with different objects
Fail navigator.credentials.get() rejects when aborted after the promise creation

View file

@ -0,0 +1,162 @@
<!DOCTYPE html>
<title>Credential Management API: create() basics.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script>
promise_test(function(t) {
return promise_rejects_dom(t, "NotSupportedError",
navigator.credentials.create());
}, "navigator.credentials.create() with no argument.");
promise_test(function(t) {
return promise_rejects_dom(t, "NotSupportedError",
navigator.credentials.create({}));
}, "navigator.credentials.create() with empty argument.");
promise_test(function(t) {
var credential_data = {
id: 'id',
password: 'pencil',
};
return navigator.credentials.create({password: credential_data})
.then(function(credential) {
assert_equals(credential.id, 'id');
assert_equals(credential.name, '');
assert_equals(credential.iconURL, '');
assert_equals(credential.type, 'password');
assert_equals(credential.password, 'pencil');
});
}, "navigator.credentials.create() with valid PasswordCredentialData");
promise_test(function(t) {
var f = document.createElement('form');
f.innerHTML = "<input type='text' name='theId' value='musterman' autocomplete='username'>"
+ "<input type='text' name='thePassword' value='sekrit' autocomplete='current-password'>"
+ "<input type='text' name='theIcon' value='https://example.com/photo' autocomplete='photo'>"
+ "<input type='text' name='theExtraField' value='extra'>"
+ "<input type='text' name='theName' value='friendly name' autocomplete='name'>";
return navigator.credentials.create({password: f})
.then(function(credential) {
assert_equals(credential.id, 'musterman');
assert_equals(credential.name, 'friendly name');
assert_equals(credential.iconURL, 'https://example.com/photo');
assert_equals(credential.type, 'password');
assert_equals(credential.password, 'sekrit');
});
}, "navigator.credentials.create() with valid HTMLFormElement");
promise_test(function(t) {
return promise_rejects_js(t, TypeError,
navigator.credentials.create({password: "bogus password data"}));
}, "navigator.credentials.create() with bogus password data");
promise_test(function(t) {
var federated_data = {
id: 'id',
provider: 'https://example.com/',
};
return navigator.credentials.create({federated: federated_data})
.then(function(credential) {
assert_equals(credential.id, 'id');
assert_equals(credential.name, '');
assert_equals(credential.iconURL, '');
assert_equals(credential.type, 'federated');
});
}, "navigator.credentials.create() with valid FederatedCredentialData");
promise_test(function(t) {
return promise_rejects_js(t, TypeError,
navigator.credentials.create({federated: "bogus federated data"}));
}, "navigator.credentials.create() with bogus federated data");
promise_test(function(t) {
return promise_rejects_js(t, TypeError,
navigator.credentials.create({publicKey: "bogus publicKey data"}));
}, "navigator.credentials.create() with bogus publicKey data");
promise_test(function(t) {
var credential_data = {
id: 'id',
password: 'pencil',
};
var federated_data = {
id: 'id',
provider: 'https://example.com/',
};
return promise_rejects_dom(t, "NotSupportedError",
navigator.credentials.create({
password: credential_data,
federated: federated_data,
}));
}, "navigator.credentials.create() with both PasswordCredentialData and FederatedCredentialData");
promise_test(function(t) {
return promise_rejects_js(t, TypeError,
navigator.credentials.create({
password: "bogus password data",
federated: "bogus federated data",
}));
}, "navigator.credentials.create() with bogus password and federated data");
promise_test(function(t) {
return promise_rejects_js(t, TypeError,
navigator.credentials.create({
federated: "bogus federated data",
publicKey: "bogus publicKey data",
}));
}, "navigator.credentials.create() with bogus federated and publicKey data");
promise_test(function(t) {
return promise_rejects_js(t, TypeError,
navigator.credentials.create({
password: "bogus password data",
publicKey: "bogus publicKey data",
}));
}, "navigator.credentials.create() with bogus password and publicKey data");
promise_test(function(t) {
return promise_rejects_js(t, TypeError,
navigator.credentials.create({
password: "bogus password data",
federated: "bogus federated data",
publicKey: "bogus publicKey data",
}));
}, "navigator.credentials.create() with bogus password, federated, and publicKey data");
promise_test(function(t) {
return promise_rejects_dom(t, "NotSupportedError",
navigator.credentials.create({bogus_key: "bogus data"}));
}, "navigator.credentials.create() with bogus data");
promise_test(function(t) {
const controller = new AbortController();
controller.abort("custom reason");
return promise_rejects_exactly(t, "custom reason",
navigator.credentials.create({ signal: controller.signal }));
}, "navigator.credentials.create() aborted with custom reason");
promise_test(async function(t) {
const reasons = [{}, [], new Error("custom error")];
for (let reason of reasons) {
const result = navigator.credentials.create({ signal: AbortSignal.abort(reason) });
await promise_rejects_exactly(t, reason, result);
}
}, "navigator.credentials.create() aborted with different objects");
promise_test(function(t) {
const error = new Error("custom error");
const controller = new AbortController();
const result = navigator.credentials.create({ signal: controller.signal });
controller.abort(error); // aborted after the promise is created
return promise_rejects_exactly(t, error, result);
}, "navigator.credentials.create() rejects when aborted after the promise creation");
</script>

View file

@ -0,0 +1,84 @@
<!DOCTYPE html>
<title>Credential Management API: get() basics.</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script>
promise_test(async (t) => {
await promise_rejects_dom(
t,
"NotSupportedError",
navigator.credentials.get()
);
await promise_rejects_dom(
t,
"NotSupportedError",
navigator.credentials.get({})
);
await promise_rejects_dom(
t,
"NotSupportedError",
navigator.credentials.get({ x: "y" })
);
await promise_rejects_dom(
t,
"NotSupportedError",
navigator.credentials.get({ x: "y", y: "z" })
);
await promise_rejects_dom(
t,
"NotSupportedError",
navigator.credentials.get({ x: "y" })
);
await promise_rejects_dom(
t,
"NotSupportedError",
navigator.credentials.get({ mediation: "required" })
);
const abortController = new AbortController();
const { signal } = abortController;
await promise_rejects_dom(
t,
"NotSupportedError",
navigator.credentials.get({ signal })
);
await promise_rejects_dom(
t,
"NotSupportedError",
navigator.credentials.get({ signal, mediation: "required" })
);
}, "Calling navigator.credentials.get() without a valid matching interface.");
promise_test(function(t) {
const controller = new AbortController();
controller.abort("custom reason");
return promise_rejects_exactly(t, "custom reason",
navigator.credentials.get({ signal: controller.signal }));
}, "navigator.credentials.get() aborted with custom reason");
promise_test(async function(t) {
const reasons = [{}, [], new Error("custom error")];
for (let reason of reasons) {
const result = navigator.credentials.get({ signal: AbortSignal.abort(reason) });
await promise_rejects_exactly(t, reason, result);
}
}, "navigator.credentials.get() aborted with different objects");
promise_test(function(t) {
const error = new Error("custom error");
const controller = new AbortController();
const result = navigator.credentials.get({ signal: controller.signal });
controller.abort(error); // aborted after the promise is created
return promise_rejects_exactly(t, error, result);
}, "navigator.credentials.get() rejects when aborted after the promise creation");
</script>