diff --git a/Libraries/LibWeb/CredentialManagement/CredentialsContainer.cpp b/Libraries/LibWeb/CredentialManagement/CredentialsContainer.cpp index bea22e9248ac..d33ce995cddb 100644 --- a/Libraries/LibWeb/CredentialManagement/CredentialsContainer.cpp +++ b/Libraries/LibWeb/CredentialManagement/CredentialsContainer.cpp @@ -5,6 +5,9 @@ */ #include +#include +#include +#include namespace Web::CredentialManagement { @@ -17,25 +20,469 @@ GC::Ref CredentialsContainer::create(JS::Realm& realm) CredentialsContainer::~CredentialsContainer() { } -// https://www.w3.org/TR/credential-management-1/#dom-credentialscontainer-get -GC::Ref 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::ErrorType::NotImplemented, "get"sv)); + auto& global = settings.global_object(); + + // TODO: 1. If settings’s relevant global object has no associated Document, return false. + // 2. Let document be settings’ relevant global object's associated Document. + auto& document = as(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 document’s node navigable. + auto navigable = document.navigable(); + + // 6. While navigable has a non-null parent: + while (navigable->parent()) { + // 1. Set navigable to navigable’s parent. + navigable = navigable->parent(); + + // 2. If navigable’s 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 CredentialsContainer::store(Credential const&) +// https://w3c.github.io/webappsec-credential-management/#credentialrequestoptions-relevant-credential-interface-objects +template +static Vector relevant_credential_interface_objects(OptionsType const& options) { - auto* realm = vm().current_realm(); - return WebIDL::create_rejected_promise_from_exception(*realm, vm().throw_completion(JS::ErrorType::NotImplemented, "store"sv)); + // 1. Let settings be the current settings object. + // 2. Let relevant interface objects be an empty set. + Vector 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: credentialInterfaceObject’s [[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 CredentialsContainer::create(CredentialCreationOptions const&) +// https://w3c.github.io/webappsec-credential-management/#algorithm-collect-known +static JS::ThrowCompletionOr>> 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::ErrorType::NotImplemented, "create"sv)); + // 1. Let possible matches be an empty set. + Vector> 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 interface’s [[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, CredentialInterface*> ask_the_user_to_choose_a_credential(CredentialRequestOptions const&, Vector> 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 interface’s [[discovery]] slot’s 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 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 settings’s relevant global object's associated Document. + auto& document = as(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.signal’s abort reason. + if (options.signal && options.signal->aborted()) + return WebIDL::create_rejected_promise(realm(), options.signal->reason()); + + // 6. Let interfaces be options’s 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 interface’s [[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 interface’s [[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 interface’s [[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: + Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm().heap(), [this, promise = GC::Root(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()) { + // 1. Set result to the result of executing result’s [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors), + // given origin, options, and sameOriginWithAncestors. + auto maybe_result = result.get()->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 global’s DOM manipulation task source to run the following substeps: + queue_global_task(HTML::Task::Source::DOMManipulation, settings.global_object(), GC::create_function(settings.heap(), [&] { + // 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() || result.has>()); + + // 8. If result is a Credential, resolve p with result. + if (result.has>()) { + WebIDL::resolve_promise(realm(), *promise, result.get>()); + return; + } + + // 9. If result is null and options.mediation is not conditional, resolve p with result. + if (result.has() && options.mediation != Bindings::CredentialMediationRequirement::Conditional) + WebIDL::resolve_promise(realm(), *promise, JS::js_null()); + })); + + // 14. React to p: + auto on_completion = GC::create_function(realm().heap(), [&settings, interfaces = move(interfaces)](JS::Value) -> WebIDL::ExceptionOr { + // 1. For each interface in interfaces: + for (auto const& interface : interfaces) { + // 1. Remove interface’s [[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 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 settings’s relevant global object's associated Document is not fully active, + // then return a promise rejected with an "InvalidStateError" DOMException. + if (!as(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 credential’s [[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 credential’s [[type]] to settings’ active credential types. + settings.active_credential_types().append(credential.type()); + + // 8. Run the following steps in parallel: + Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm().heap(), [this, promise = GC::Root(promise), &settings, &credential, same_origin_with_ancestors] { + HTML::TemporaryExecutionContext execution_context { realm(), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + + // 1. Execute credential’s 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 global’s DOM manipulation task source to run the following substeps: + queue_global_task(HTML::Task::Source::DOMManipulation, settings.global_object(), GC::create_function(settings.heap(), [&] { + // 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 { + // 1. Remove credential’s [[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 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(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 interface’s [[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.signal’s 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 + 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 settings’s 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: + Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm().heap(), [this, promise = GC::Root(promise), &global, &document, 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 global’s DOM manipulation task source to run the following substeps: + queue_global_task(HTML::Task::Source::DOMManipulation, global, GC::create_function(document.heap(), [&] { + // 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()) { + WebIDL::resolve_promise(realm(), *promise, JS::js_null()); + return; + } + if (r.has>()) { + auto& credential = r.get>(); + 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>()); + + // 4. Queue a task on global’s DOM manipulation task source to run the following substeps: + auto& r_algo = r.get>(); + queue_global_task(HTML::Task::Source::DOMManipulation, global, GC::create_function(document.heap(), [this, &global, promise = GC::Root(promise), r_algo = GC::Root(r_algo)] { + // 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 { + // 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 diff --git a/Tests/LibWeb/Text/expected/wpt-import/credential-management/credentialscontainer-create-basics.https.txt b/Tests/LibWeb/Text/expected/wpt-import/credential-management/credentialscontainer-create-basics.https.txt index c82dc9117c96..c12ef7cdb3a7 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/credential-management/credentialscontainer-create-basics.https.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/credential-management/credentialscontainer-create-basics.https.txt @@ -2,10 +2,10 @@ Harness status: OK Found 17 tests -6 Pass -11 Fail -Fail navigator.credentials.create() with no argument. -Fail navigator.credentials.create() with empty argument. +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 @@ -17,7 +17,7 @@ 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 -Fail navigator.credentials.create() with bogus data -Fail navigator.credentials.create() aborted with custom reason -Fail navigator.credentials.create() aborted with different objects +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 \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/credential-management/credentialscontainer-get-basics.https.txt b/Tests/LibWeb/Text/expected/wpt-import/credential-management/credentialscontainer-get-basics.https.txt index 467787b09d38..4d2dd8b9cf82 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/credential-management/credentialscontainer-get-basics.https.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/credential-management/credentialscontainer-get-basics.https.txt @@ -2,8 +2,9 @@ Harness status: OK Found 4 tests -4 Fail -Fail Calling navigator.credentials.get() without a valid matching interface. -Fail navigator.credentials.get() aborted with custom reason -Fail navigator.credentials.get() aborted with different objects +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 \ No newline at end of file