From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- dom/credentialmanagement/Credential.cpp | 40 + dom/credentialmanagement/Credential.h | 50 + dom/credentialmanagement/CredentialsContainer.cpp | 310 ++++++ dom/credentialmanagement/CredentialsContainer.h | 53 + .../identity/IPCIdentityCredential.ipdlh | 18 + .../identity/IdentityCredential.cpp | 1114 ++++++++++++++++++++ .../identity/IdentityCredential.h | 314 ++++++ .../IdentityCredentialSerializationHelpers.h | 48 + .../identity/IdentityNetworkHelpers.h | 113 ++ dom/credentialmanagement/identity/moz.build | 28 + .../identity/tests/browser/browser.toml | 21 + .../browser/browser_close_prompt_on_timeout.js | 63 ++ .../browser_single_concurrent_identity_request.js | 51 + .../identity/tests/browser/server_accounts.json | 12 + .../tests/browser/server_accounts.json^headers^ | 3 + .../identity/tests/browser/server_idtoken.json | 1 + .../tests/browser/server_idtoken.json^headers^ | 3 + .../identity/tests/browser/server_manifest.json | 5 + .../tests/browser/server_manifest.json^headers^ | 2 + .../identity/tests/browser/server_metadata.json | 4 + .../tests/browser/server_metadata.json^headers^ | 2 + .../identity/tests/mochitest/head.js | 24 + .../tests/mochitest/helper_set_cookie.html | 8 + .../mochitest/helper_set_cookie.html^headers^ | 1 + .../identity/tests/mochitest/mochitest.toml | 67 ++ .../mochitest/server_accounts_error_accounts.sjs | 9 + .../mochitest/server_accounts_error_idtoken.sjs | 15 + .../server_accounts_redirect_accounts.sjs | 10 + .../mochitest/server_accounts_redirect_idtoken.sjs | 15 + .../mochitest/server_idtoken_error_accounts.sjs | 24 + .../mochitest/server_idtoken_error_idtoken.sjs | 9 + .../mochitest/server_idtoken_redirect_accounts.sjs | 24 + .../mochitest/server_idtoken_redirect_idtoken.sjs | 10 + .../identity/tests/mochitest/server_manifest.sjs | 44 + .../server_manifest_wrong_provider_in_manifest.sjs | 19 + .../identity/tests/mochitest/server_metadata.json | 4 + .../tests/mochitest/server_metadata.json^headers^ | 2 + .../mochitest/server_no_accounts_accounts.sjs | 38 + .../tests/mochitest/server_no_accounts_idtoken.sjs | 66 ++ .../tests/mochitest/server_simple_accounts.sjs | 47 + .../tests/mochitest/server_simple_idtoken.sjs | 66 ++ .../mochitest/server_two_accounts_accounts.sjs | 55 + .../mochitest/server_two_accounts_idtoken.sjs | 66 ++ .../mochitest/server_two_providers_accounts.sjs | 48 + .../mochitest/server_two_providers_idtoken.sjs | 59 ++ .../tests/mochitest/test_accounts_error.html | 37 + .../tests/mochitest/test_accounts_redirect.html | 37 + .../tests/mochitest/test_delay_reject.html | 39 + .../tests/mochitest/test_empty_provider_list.html | 34 + .../mochitest/test_get_without_providers.html | 32 + .../tests/mochitest/test_idtoken_error.html | 37 + .../tests/mochitest/test_idtoken_redirect.html | 37 + .../identity/tests/mochitest/test_mediation.html | 38 + .../identity/tests/mochitest/test_no_accounts.html | 37 + .../identity/tests/mochitest/test_simple.html | 46 + .../tests/mochitest/test_two_accounts.html | 46 + .../tests/mochitest/test_two_providers.html | 52 + .../mochitest/test_wrong_provider_in_manifest.html | 37 + .../identity/tests/mochitest/web-identity | 1 + .../identity/tests/mochitest/web-identity^headers^ | 2 + dom/credentialmanagement/moz.build | 28 + .../tests/browser/browser.toml | 3 + .../tests/browser/browser_active_document.js | 139 +++ .../tests/crashtests/bug1691963.html | 28 + .../tests/crashtests/crashtests.list | 1 + .../tests/mochitest/frame_credman_iframes.html | 105 ++ .../tests/mochitest/mochitest.toml | 14 + .../tests/mochitest/test_credman_empty_option.html | 40 + .../tests/mochitest/test_credman_iframes.html | 88 ++ 69 files changed, 3943 insertions(+) create mode 100644 dom/credentialmanagement/Credential.cpp create mode 100644 dom/credentialmanagement/Credential.h create mode 100644 dom/credentialmanagement/CredentialsContainer.cpp create mode 100644 dom/credentialmanagement/CredentialsContainer.h create mode 100644 dom/credentialmanagement/identity/IPCIdentityCredential.ipdlh create mode 100644 dom/credentialmanagement/identity/IdentityCredential.cpp create mode 100644 dom/credentialmanagement/identity/IdentityCredential.h create mode 100644 dom/credentialmanagement/identity/IdentityCredentialSerializationHelpers.h create mode 100644 dom/credentialmanagement/identity/IdentityNetworkHelpers.h create mode 100644 dom/credentialmanagement/identity/moz.build create mode 100644 dom/credentialmanagement/identity/tests/browser/browser.toml create mode 100644 dom/credentialmanagement/identity/tests/browser/browser_close_prompt_on_timeout.js create mode 100644 dom/credentialmanagement/identity/tests/browser/browser_single_concurrent_identity_request.js create mode 100644 dom/credentialmanagement/identity/tests/browser/server_accounts.json create mode 100644 dom/credentialmanagement/identity/tests/browser/server_accounts.json^headers^ create mode 100644 dom/credentialmanagement/identity/tests/browser/server_idtoken.json create mode 100644 dom/credentialmanagement/identity/tests/browser/server_idtoken.json^headers^ create mode 100644 dom/credentialmanagement/identity/tests/browser/server_manifest.json create mode 100644 dom/credentialmanagement/identity/tests/browser/server_manifest.json^headers^ create mode 100644 dom/credentialmanagement/identity/tests/browser/server_metadata.json create mode 100644 dom/credentialmanagement/identity/tests/browser/server_metadata.json^headers^ create mode 100644 dom/credentialmanagement/identity/tests/mochitest/head.js create mode 100644 dom/credentialmanagement/identity/tests/mochitest/helper_set_cookie.html create mode 100644 dom/credentialmanagement/identity/tests/mochitest/helper_set_cookie.html^headers^ create mode 100644 dom/credentialmanagement/identity/tests/mochitest/mochitest.toml create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_accounts_error_accounts.sjs create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_accounts_error_idtoken.sjs create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_accounts_redirect_accounts.sjs create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_accounts_redirect_idtoken.sjs create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_idtoken_error_accounts.sjs create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_idtoken_error_idtoken.sjs create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_idtoken_redirect_accounts.sjs create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_idtoken_redirect_idtoken.sjs create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_manifest_wrong_provider_in_manifest.sjs create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_metadata.json create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_metadata.json^headers^ create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_no_accounts_accounts.sjs create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_no_accounts_idtoken.sjs create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_simple_accounts.sjs create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_simple_idtoken.sjs create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_two_accounts_accounts.sjs create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_two_accounts_idtoken.sjs create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_two_providers_accounts.sjs create mode 100644 dom/credentialmanagement/identity/tests/mochitest/server_two_providers_idtoken.sjs create mode 100644 dom/credentialmanagement/identity/tests/mochitest/test_accounts_error.html create mode 100644 dom/credentialmanagement/identity/tests/mochitest/test_accounts_redirect.html create mode 100644 dom/credentialmanagement/identity/tests/mochitest/test_delay_reject.html create mode 100644 dom/credentialmanagement/identity/tests/mochitest/test_empty_provider_list.html create mode 100644 dom/credentialmanagement/identity/tests/mochitest/test_get_without_providers.html create mode 100644 dom/credentialmanagement/identity/tests/mochitest/test_idtoken_error.html create mode 100644 dom/credentialmanagement/identity/tests/mochitest/test_idtoken_redirect.html create mode 100644 dom/credentialmanagement/identity/tests/mochitest/test_mediation.html create mode 100644 dom/credentialmanagement/identity/tests/mochitest/test_no_accounts.html create mode 100644 dom/credentialmanagement/identity/tests/mochitest/test_simple.html create mode 100644 dom/credentialmanagement/identity/tests/mochitest/test_two_accounts.html create mode 100644 dom/credentialmanagement/identity/tests/mochitest/test_two_providers.html create mode 100644 dom/credentialmanagement/identity/tests/mochitest/test_wrong_provider_in_manifest.html create mode 100644 dom/credentialmanagement/identity/tests/mochitest/web-identity create mode 100644 dom/credentialmanagement/identity/tests/mochitest/web-identity^headers^ create mode 100644 dom/credentialmanagement/moz.build create mode 100644 dom/credentialmanagement/tests/browser/browser.toml create mode 100644 dom/credentialmanagement/tests/browser/browser_active_document.js create mode 100644 dom/credentialmanagement/tests/crashtests/bug1691963.html create mode 100644 dom/credentialmanagement/tests/crashtests/crashtests.list create mode 100644 dom/credentialmanagement/tests/mochitest/frame_credman_iframes.html create mode 100644 dom/credentialmanagement/tests/mochitest/mochitest.toml create mode 100644 dom/credentialmanagement/tests/mochitest/test_credman_empty_option.html create mode 100644 dom/credentialmanagement/tests/mochitest/test_credman_iframes.html (limited to 'dom/credentialmanagement') diff --git a/dom/credentialmanagement/Credential.cpp b/dom/credentialmanagement/Credential.cpp new file mode 100644 index 0000000000..5314008944 --- /dev/null +++ b/dom/credentialmanagement/Credential.cpp @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/Credential.h" +#include "mozilla/dom/CredentialManagementBinding.h" +#include "nsCycleCollectionParticipant.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Credential, mParent) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Credential) +NS_IMPL_CYCLE_COLLECTING_RELEASE(Credential) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Credential) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +Credential::Credential(nsPIDOMWindowInner* aParent) : mParent(aParent) {} + +Credential::~Credential() = default; + +JSObject* Credential::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return Credential_Binding::Wrap(aCx, this, aGivenProto); +} + +void Credential::GetId(nsAString& aId) const { aId.Assign(mId); } + +void Credential::GetType(nsAString& aType) const { aType.Assign(mType); } + +void Credential::SetId(const nsAString& aId) { mId.Assign(aId); } + +void Credential::SetType(const nsAString& aType) { mType.Assign(aType); } + +} // namespace mozilla::dom diff --git a/dom/credentialmanagement/Credential.h b/dom/credentialmanagement/Credential.h new file mode 100644 index 0000000000..bb5fbd9539 --- /dev/null +++ b/dom/credentialmanagement/Credential.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_Credential_h +#define mozilla_dom_Credential_h + +#include "mozilla/dom/CredentialManagementBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "nsPIDOMWindow.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class Credential : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Credential) + + public: + explicit Credential(nsPIDOMWindowInner* aParent); + + protected: + virtual ~Credential(); + + public: + nsISupports* GetParentObject() const { return mParent; } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + void GetId(nsAString& aId) const; + + void GetType(nsAString& aType) const; + + void SetId(const nsAString& aId); + + void SetType(const nsAString& aType); + + private: + nsCOMPtr mParent; + nsAutoString mId; + nsAutoString mType; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_Credential_h diff --git a/dom/credentialmanagement/CredentialsContainer.cpp b/dom/credentialmanagement/CredentialsContainer.cpp new file mode 100644 index 0000000000..9a39d66527 --- /dev/null +++ b/dom/credentialmanagement/CredentialsContainer.cpp @@ -0,0 +1,310 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/Credential.h" +#include "mozilla/dom/CredentialsContainer.h" +#include "mozilla/dom/FeaturePolicyUtils.h" +#include "mozilla/dom/IdentityCredential.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_security.h" +#include "mozilla/dom/WebAuthnManager.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "mozilla/dom/WindowContext.h" +#include "nsContentUtils.h" +#include "nsFocusManager.h" +#include "nsIDocShell.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CredentialsContainer, mParent, mManager) +NS_IMPL_CYCLE_COLLECTING_ADDREF(CredentialsContainer) +NS_IMPL_CYCLE_COLLECTING_RELEASE(CredentialsContainer) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CredentialsContainer) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +already_AddRefed CreatePromise(nsPIDOMWindowInner* aParent, + ErrorResult& aRv) { + MOZ_ASSERT(aParent); + nsCOMPtr global = do_QueryInterface(aParent); + if (NS_WARN_IF(!global)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + RefPtr promise = Promise::Create(global, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + return promise.forget(); +} + +already_AddRefed CreateAndRejectWithNotAllowed( + nsPIDOMWindowInner* aParent, ErrorResult& aRv) { + MOZ_ASSERT(aParent); + RefPtr promise = CreatePromise(aParent, aRv); + if (!promise) { + return nullptr; + } + promise->MaybeRejectWithNotAllowedError( + "CredentialContainer request is not allowed."_ns); + return promise.forget(); +} + +already_AddRefed CreateAndRejectWithNotSupported( + nsPIDOMWindowInner* aParent, ErrorResult& aRv) { + MOZ_ASSERT(aParent); + RefPtr promise = CreatePromise(aParent, aRv); + if (!promise) { + return nullptr; + } + promise->MaybeRejectWithNotSupportedError( + "CredentialContainer request is not supported."_ns); + return promise.forget(); +} + +static bool IsInActiveTab(nsPIDOMWindowInner* aParent) { + // Returns whether aParent is an inner window somewhere in the active tab. + // The active tab is the selected (i.e. visible) tab in the focused window. + MOZ_ASSERT(aParent); + + RefPtr doc = aParent->GetExtantDoc(); + if (NS_WARN_IF(!doc)) { + return false; + } + + return IsInActiveTab(doc); +} + +static bool ConsumeUserActivation(nsPIDOMWindowInner* aParent) { + // Returns whether aParent has transient activation, and consumes the + // activation. + MOZ_ASSERT(aParent); + + RefPtr doc = aParent->GetExtantDoc(); + if (NS_WARN_IF(!doc)) { + return false; + } + + return doc->ConsumeTransientUserGestureActivation(); +} + +static bool IsSameOriginWithAncestors(nsPIDOMWindowInner* aParent) { + // This method returns true if aParent is either not in a frame / iframe, or + // is in a frame or iframe and all ancestors for aParent are the same origin. + // This is useful for Credential Management because we need to prohibit + // iframes, but not break mochitests (which use iframes to embed the tests). + MOZ_ASSERT(aParent); + + WindowGlobalChild* wgc = aParent->GetWindowGlobalChild(); + + // If there's no WindowGlobalChild, the inner window has already been + // destroyed, so fail safe and return false. + if (!wgc) { + return false; + } + + // Check that all ancestors are the same origin, repeating until we find a + // null parent + for (WindowContext* parentContext = + wgc->WindowContext()->GetParentWindowContext(); + parentContext; parentContext = parentContext->GetParentWindowContext()) { + if (!wgc->IsSameOriginWith(parentContext)) { + // same-origin policy is violated + return false; + } + } + + return true; +} + +CredentialsContainer::CredentialsContainer(nsPIDOMWindowInner* aParent) + : mParent(aParent), mActiveIdentityRequest(false) { + MOZ_ASSERT(aParent); +} + +CredentialsContainer::~CredentialsContainer() = default; + +void CredentialsContainer::EnsureWebAuthnManager() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mManager) { + mManager = new WebAuthnManager(mParent); + } +} + +already_AddRefed CredentialsContainer::GetWebAuthnManager() { + MOZ_ASSERT(NS_IsMainThread()); + + EnsureWebAuthnManager(); + RefPtr ref = mManager; + return ref.forget(); +} + +JSObject* CredentialsContainer::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return CredentialsContainer_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed CredentialsContainer::Get( + const CredentialRequestOptions& aOptions, ErrorResult& aRv) { + uint64_t totalOptions = 0; + if (aOptions.mPublicKey.WasPassed() && + StaticPrefs::security_webauth_webauthn()) { + totalOptions += 1; + } + if (aOptions.mIdentity.WasPassed() && + StaticPrefs::dom_security_credentialmanagement_identity_enabled()) { + totalOptions += 1; + } + if (totalOptions > 1) { + return CreateAndRejectWithNotSupported(mParent, aRv); + } + + bool conditionallyMediated = + aOptions.mMediation == CredentialMediationRequirement::Conditional; + if (aOptions.mPublicKey.WasPassed() && + StaticPrefs::security_webauth_webauthn()) { + MOZ_ASSERT(mParent); + if (!FeaturePolicyUtils::IsFeatureAllowed( + mParent->GetExtantDoc(), u"publickey-credentials-get"_ns) || + !IsInActiveTab(mParent)) { + return CreateAndRejectWithNotAllowed(mParent, aRv); + } + + if (conditionallyMediated && + !StaticPrefs::security_webauthn_enable_conditional_mediation()) { + RefPtr promise = CreatePromise(mParent, aRv); + if (!promise) { + return nullptr; + } + promise->MaybeRejectWithTypeError( + "mediation", "conditional", "CredentialMediationRequirement"); + return promise.forget(); + } + + EnsureWebAuthnManager(); + return mManager->GetAssertion(aOptions.mPublicKey.Value(), + conditionallyMediated, aOptions.mSignal, aRv); + } + + if (aOptions.mIdentity.WasPassed() && + StaticPrefs::dom_security_credentialmanagement_identity_enabled()) { + RefPtr promise = CreatePromise(mParent, aRv); + if (!promise) { + return nullptr; + } + + if (conditionallyMediated) { + promise->MaybeRejectWithTypeError( + "mediation", "conditional", "CredentialMediationRequirement"); + return promise.forget(); + } + + if (mActiveIdentityRequest) { + promise->MaybeRejectWithInvalidStateError( + "Concurrent 'identity' credentials.get requests are not supported."_ns); + return promise.forget(); + } + mActiveIdentityRequest = true; + + RefPtr self = this; + + IdentityCredential::DiscoverFromExternalSource( + mParent, aOptions, IsSameOriginWithAncestors(mParent)) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self, promise](const RefPtr& credential) { + self->mActiveIdentityRequest = false; + promise->MaybeResolve(credential); + }, + [self, promise](nsresult error) { + self->mActiveIdentityRequest = false; + promise->MaybeReject(error); + }); + + return promise.forget(); + } + + return CreateAndRejectWithNotSupported(mParent, aRv); +} + +already_AddRefed CredentialsContainer::Create( + const CredentialCreationOptions& aOptions, ErrorResult& aRv) { + // Count the types of options provided. Must not be >1. + uint64_t totalOptions = 0; + if (aOptions.mPublicKey.WasPassed() && + StaticPrefs::security_webauth_webauthn()) { + totalOptions += 1; + } + if (totalOptions > 1) { + return CreateAndRejectWithNotSupported(mParent, aRv); + } + + if (aOptions.mPublicKey.WasPassed() && + StaticPrefs::security_webauth_webauthn()) { + MOZ_ASSERT(mParent); + // In a cross-origin iframe this request consumes user activation, i.e. + // subsequent requests cannot be made without further user interaction. + // See step 2.2 of https://w3c.github.io/webauthn/#sctn-createCredential + bool hasRequiredActivation = + IsInActiveTab(mParent) && + (IsSameOriginWithAncestors(mParent) || ConsumeUserActivation(mParent)); + if (!FeaturePolicyUtils::IsFeatureAllowed( + mParent->GetExtantDoc(), u"publickey-credentials-create"_ns) || + !hasRequiredActivation) { + return CreateAndRejectWithNotAllowed(mParent, aRv); + } + + EnsureWebAuthnManager(); + return mManager->MakeCredential(aOptions.mPublicKey.Value(), + aOptions.mSignal, aRv); + } + + return CreateAndRejectWithNotSupported(mParent, aRv); +} + +already_AddRefed CredentialsContainer::Store( + const Credential& aCredential, ErrorResult& aRv) { + nsString type; + aCredential.GetType(type); + if (type.EqualsLiteral("public-key") && + StaticPrefs::security_webauth_webauthn()) { + if (!IsSameOriginWithAncestors(mParent) || !IsInActiveTab(mParent)) { + return CreateAndRejectWithNotAllowed(mParent, aRv); + } + + EnsureWebAuthnManager(); + return mManager->Store(aCredential, aRv); + } + + if (type.EqualsLiteral("identity") && + StaticPrefs::dom_security_credentialmanagement_identity_enabled()) { + return CreateAndRejectWithNotSupported(mParent, aRv); + } + + return CreateAndRejectWithNotSupported(mParent, aRv); +} + +already_AddRefed CredentialsContainer::PreventSilentAccess( + ErrorResult& aRv) { + nsCOMPtr global = do_QueryInterface(mParent); + if (NS_WARN_IF(!global)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr promise = Promise::Create(global, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + promise->MaybeResolveWithUndefined(); + return promise.forget(); +} + +} // namespace mozilla::dom diff --git a/dom/credentialmanagement/CredentialsContainer.h b/dom/credentialmanagement/CredentialsContainer.h new file mode 100644 index 0000000000..1c4c53bd9d --- /dev/null +++ b/dom/credentialmanagement/CredentialsContainer.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_CredentialsContainer_h +#define mozilla_dom_CredentialsContainer_h + +#include "mozilla/dom/CredentialManagementBinding.h" + +namespace mozilla::dom { + +class WebAuthnManager; + +class CredentialsContainer final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(CredentialsContainer) + + explicit CredentialsContainer(nsPIDOMWindowInner* aParent); + + nsPIDOMWindowInner* GetParentObject() const { return mParent; } + + already_AddRefed GetWebAuthnManager(); + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + already_AddRefed Get(const CredentialRequestOptions& aOptions, + ErrorResult& aRv); + + already_AddRefed Create(const CredentialCreationOptions& aOptions, + ErrorResult& aRv); + + already_AddRefed Store(const Credential& aCredential, + ErrorResult& aRv); + + already_AddRefed PreventSilentAccess(ErrorResult& aRv); + + private: + ~CredentialsContainer(); + + void EnsureWebAuthnManager(); + + nsCOMPtr mParent; + RefPtr mManager; + bool mActiveIdentityRequest; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_CredentialsContainer_h diff --git a/dom/credentialmanagement/identity/IPCIdentityCredential.ipdlh b/dom/credentialmanagement/identity/IPCIdentityCredential.ipdlh new file mode 100644 index 0000000000..3502a58b4f --- /dev/null +++ b/dom/credentialmanagement/identity/IPCIdentityCredential.ipdlh @@ -0,0 +1,18 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +namespace mozilla { +namespace dom { + +struct IPCIdentityCredential +{ + nsString id; + nsString type; + nsString token; +}; + +} +} diff --git a/dom/credentialmanagement/identity/IdentityCredential.cpp b/dom/credentialmanagement/identity/IdentityCredential.cpp new file mode 100644 index 0000000000..182974e81d --- /dev/null +++ b/dom/credentialmanagement/identity/IdentityCredential.cpp @@ -0,0 +1,1114 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Fetch.h" +#include "mozilla/dom/IdentityCredential.h" +#include "mozilla/dom/IdentityNetworkHelpers.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/Promise-inl.h" +#include "mozilla/dom/Request.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "mozilla/Components.h" +#include "mozilla/ExpandedPrincipal.h" +#include "mozilla/NullPrincipal.h" +#include "nsEffectiveTLDService.h" +#include "nsIGlobalObject.h" +#include "nsIIdentityCredentialPromptService.h" +#include "nsIIdentityCredentialStorageService.h" +#include "nsITimer.h" +#include "nsIXPConnect.h" +#include "nsNetUtil.h" +#include "nsStringStream.h" +#include "nsTArray.h" +#include "nsURLHelper.h" + +namespace mozilla::dom { + +IdentityCredential::~IdentityCredential() = default; + +JSObject* IdentityCredential::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return IdentityCredential_Binding::Wrap(aCx, this, aGivenProto); +} + +IdentityCredential::IdentityCredential(nsPIDOMWindowInner* aParent) + : Credential(aParent) {} + +void IdentityCredential::CopyValuesFrom(const IPCIdentityCredential& aOther) { + this->SetToken(aOther.token()); + this->SetId(aOther.id()); + this->SetType(aOther.type()); +} + +IPCIdentityCredential IdentityCredential::MakeIPCIdentityCredential() { + nsString token, id, type; + GetToken(token); + GetId(id); + GetType(type); + IPCIdentityCredential result; + result.token() = token; + result.id() = id; + result.type() = type; + return result; +} + +void IdentityCredential::GetToken(nsAString& aToken) const { + aToken.Assign(mToken); +} +void IdentityCredential::SetToken(const nsAString& aToken) { + mToken.Assign(aToken); +} + +// static +RefPtr +IdentityCredential::DiscoverFromExternalSource( + nsPIDOMWindowInner* aParent, const CredentialRequestOptions& aOptions, + bool aSameOriginWithAncestors) { + MOZ_ASSERT(XRE_IsContentProcess()); + MOZ_ASSERT(aParent); + // Prevent origin confusion by requiring no cross domain iframes + // in this one's ancestry + if (!aSameOriginWithAncestors) { + return IdentityCredential::GetIdentityCredentialPromise::CreateAndReject( + NS_ERROR_DOM_NOT_ALLOWED_ERR, __func__); + } + + Document* parentDocument = aParent->GetExtantDoc(); + if (!parentDocument) { + return IdentityCredential::GetIdentityCredentialPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + + // Kick the request off to the main process and translate the result to the + // expected type when we get a result. + MOZ_ASSERT(aOptions.mIdentity.WasPassed()); + RefPtr wgc = aParent->GetWindowGlobalChild(); + MOZ_ASSERT(wgc); + RefPtr credential = new IdentityCredential(aParent); + return wgc + ->SendDiscoverIdentityCredentialFromExternalSource( + aOptions.mIdentity.Value()) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [credential](const WindowGlobalChild:: + DiscoverIdentityCredentialFromExternalSourcePromise:: + ResolveValueType& aResult) { + if (aResult.isSome()) { + credential->CopyValuesFrom(aResult.value()); + return IdentityCredential::GetIdentityCredentialPromise:: + CreateAndResolve(credential, __func__); + } + return IdentityCredential::GetIdentityCredentialPromise:: + CreateAndReject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); + }, + [](const WindowGlobalChild:: + DiscoverIdentityCredentialFromExternalSourcePromise:: + RejectValueType& aResult) { + return IdentityCredential::GetIdentityCredentialPromise:: + CreateAndReject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); + }); +} + +// static +RefPtr +IdentityCredential::DiscoverFromExternalSourceInMainProcess( + nsIPrincipal* aPrincipal, CanonicalBrowsingContext* aBrowsingContext, + const IdentityCredentialRequestOptions& aOptions) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aBrowsingContext); + + // Make sure we have providers. + if (!aOptions.mProviders.WasPassed() || + aOptions.mProviders.Value().Length() < 1) { + return IdentityCredential::GetIPCIdentityCredentialPromise::CreateAndReject( + NS_ERROR_DOM_NOT_ALLOWED_ERR, __func__); + } + + RefPtr result = + new IdentityCredential::GetIPCIdentityCredentialPromise::Private( + __func__); + + nsCOMPtr principal(aPrincipal); + RefPtr browsingContext(aBrowsingContext); + + RefPtr timeout; + if (StaticPrefs:: + dom_security_credentialmanagement_identity_reject_delay_enabled()) { + nsresult rv = NS_NewTimerWithCallback( + getter_AddRefs(timeout), + [=](auto) { + if (!result->IsResolved()) { + result->Reject(NS_ERROR_DOM_NETWORK_ERR, __func__); + } + IdentityCredential::CloseUserInterface(browsingContext); + }, + StaticPrefs:: + dom_security_credentialmanagement_identity_reject_delay_duration_ms(), + nsITimer::TYPE_ONE_SHOT, "IdentityCredentialTimeoutCallback"); + if (NS_WARN_IF(NS_FAILED(rv))) { + result->Reject(NS_ERROR_FAILURE, __func__); + return result.forget(); + } + } + + // Construct an array of requests to fetch manifests for every provider. + // We need this to show their branding information + nsTArray> manifestPromises; + for (const IdentityProviderConfig& provider : aOptions.mProviders.Value()) { + RefPtr manifest = + IdentityCredential::CheckRootManifest(aPrincipal, provider) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [provider, principal](bool valid) { + if (valid) { + return IdentityCredential::FetchInternalManifest(principal, + provider); + } + return IdentityCredential::GetManifestPromise:: + CreateAndReject(NS_ERROR_FAILURE, __func__); + }, + [](nsresult error) { + return IdentityCredential::GetManifestPromise:: + CreateAndReject(error, __func__); + }); + manifestPromises.AppendElement(manifest); + } + + // We use AllSettled here so that failures will be included- we use default + // values there. + GetManifestPromise::AllSettled(GetCurrentSerialEventTarget(), + manifestPromises) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [browsingContext, aOptions]( + const GetManifestPromise::AllSettledPromiseType::ResolveValueType& + aResults) { + // Convert the + // GetManifestPromise::AllSettledPromiseType::ResolveValueType to a + // Sequence + CopyableTArray::ResolveOrRejectValue> + results = aResults; + const Sequence::ResolveOrRejectValue> + resultsSequence(std::move(results)); + // The user picks from the providers + return PromptUserToSelectProvider( + browsingContext, aOptions.mProviders.Value(), resultsSequence); + }, + [](bool error) { + return IdentityCredential:: + GetIdentityProviderConfigWithManifestPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [principal, browsingContext]( + const IdentityProviderConfigWithManifest& providerAndManifest) { + IdentityProviderAPIConfig manifest; + IdentityProviderConfig provider; + std::tie(provider, manifest) = providerAndManifest; + return IdentityCredential::CreateCredential( + principal, browsingContext, provider, manifest); + }, + [](nsresult error) { + return IdentityCredential::GetIPCIdentityCredentialPromise:: + CreateAndReject(error, __func__); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [result, timeout = std::move(timeout)]( + const IdentityCredential::GetIPCIdentityCredentialPromise:: + ResolveOrRejectValue&& value) { + // Resolve the result + result->ResolveOrReject(value, __func__); + + // Cancel the timer (if it is still pending) and + // release the hold on the variables leaked into the timer. + if (timeout && + StaticPrefs:: + dom_security_credentialmanagement_identity_reject_delay_enabled()) { + timeout->Cancel(); + } + }); + + return result; +} + +// static +RefPtr +IdentityCredential::CreateCredential( + nsIPrincipal* aPrincipal, BrowsingContext* aBrowsingContext, + const IdentityProviderConfig& aProvider, + const IdentityProviderAPIConfig& aManifest) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aBrowsingContext); + + nsCOMPtr argumentPrincipal = aPrincipal; + RefPtr browsingContext(aBrowsingContext); + + return IdentityCredential::FetchAccountList(argumentPrincipal, aProvider, + aManifest) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [argumentPrincipal, browsingContext, aProvider]( + const std::tuple& promiseResult) { + IdentityProviderAPIConfig currentManifest; + IdentityProviderAccountList accountList; + std::tie(currentManifest, accountList) = promiseResult; + if (!accountList.mAccounts.WasPassed() || + accountList.mAccounts.Value().Length() == 0) { + return IdentityCredential::GetAccountPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + return PromptUserToSelectAccount(browsingContext, accountList, + aProvider, currentManifest); + }, + [](nsresult error) { + return IdentityCredential::GetAccountPromise::CreateAndReject( + error, __func__); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [argumentPrincipal, browsingContext, aProvider]( + const std::tuple& promiseResult) { + IdentityProviderAPIConfig currentManifest; + IdentityProviderAccount account; + std::tie(currentManifest, account) = promiseResult; + return IdentityCredential::PromptUserWithPolicy( + browsingContext, argumentPrincipal, account, currentManifest, + aProvider); + }, + [](nsresult error) { + return IdentityCredential::GetAccountPromise::CreateAndReject( + error, __func__); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [argumentPrincipal, aProvider]( + const std::tuple& promiseResult) { + IdentityProviderAPIConfig currentManifest; + IdentityProviderAccount account; + std::tie(currentManifest, account) = promiseResult; + return IdentityCredential::FetchToken(argumentPrincipal, aProvider, + currentManifest, account); + }, + [](nsresult error) { + return IdentityCredential::GetTokenPromise::CreateAndReject( + error, __func__); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [aProvider]( + const std::tuple& + promiseResult) { + IdentityProviderToken token; + IdentityProviderAccount account; + std::tie(token, account) = promiseResult; + IPCIdentityCredential credential; + credential.token() = token.mToken; + credential.id() = account.mId; + credential.type() = u"identity"_ns; + return IdentityCredential::GetIPCIdentityCredentialPromise:: + CreateAndResolve(credential, __func__); + }, + [browsingContext](nsresult error) { + CloseUserInterface(browsingContext); + return IdentityCredential::GetIPCIdentityCredentialPromise:: + CreateAndReject(error, __func__); + }); +} + +// static +RefPtr +IdentityCredential::CheckRootManifest(nsIPrincipal* aPrincipal, + const IdentityProviderConfig& aProvider) { + MOZ_ASSERT(XRE_IsParentProcess()); + + if (StaticPrefs:: + dom_security_credentialmanagement_identity_test_ignore_well_known()) { + return IdentityCredential::ValidationPromise::CreateAndResolve(true, + __func__); + } + + // Build the URL + nsCString configLocation = aProvider.mConfigURL; + nsCOMPtr configURI; + nsresult rv = NS_NewURI(getter_AddRefs(configURI), configLocation); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::ValidationPromise::CreateAndReject(rv, __func__); + } + RefPtr etld = nsEffectiveTLDService::GetInstance(); + if (!etld) { + return IdentityCredential::ValidationPromise::CreateAndReject( + NS_ERROR_SERVICE_NOT_AVAILABLE, __func__); + } + nsCString manifestURIString; + rv = etld->GetSite(configURI, manifestURIString); + if (NS_FAILED(rv)) { + return IdentityCredential::ValidationPromise::CreateAndReject( + NS_ERROR_INVALID_ARG, __func__); + } + manifestURIString.AppendLiteral("/.well-known/web-identity"); + + // Create the global + RefPtr nullPrincipal = + NullPrincipal::CreateWithInheritedAttributes(aPrincipal); + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + nsCOMPtr global; + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + JS::Rooted sandbox(cx); + rv = xpc->CreateSandbox(cx, nullPrincipal, sandbox.address()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::ValidationPromise::CreateAndReject(rv, __func__); + } + MOZ_ASSERT(JS_IsGlobalObject(sandbox)); + global = xpc::NativeGlobal(sandbox); + if (NS_WARN_IF(!global)) { + return IdentityCredential::ValidationPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + + // Create a new request + constexpr auto fragment = ""_ns; + auto internalRequest = + MakeSafeRefPtr(manifestURIString, fragment); + internalRequest->SetCredentialsMode(RequestCredentials::Omit); + internalRequest->SetReferrerPolicy(ReferrerPolicy::No_referrer); + internalRequest->SetMode(RequestMode::Cors); + internalRequest->SetCacheMode(RequestCache::No_cache); + internalRequest->SetHeaders(new InternalHeaders(HeadersGuardEnum::Request)); + internalRequest->OverrideContentPolicyType( + nsContentPolicyType::TYPE_WEB_IDENTITY); + RefPtr request = + new Request(global, std::move(internalRequest), nullptr); + + return FetchJSONStructure(request)->Then( + GetCurrentSerialEventTarget(), __func__, + [aProvider](const IdentityProviderWellKnown& manifest) { + // Make sure there is only one provider URL + if (manifest.mProvider_urls.Length() != 1) { + return IdentityCredential::ValidationPromise::CreateAndResolve( + false, __func__); + } + + // Resolve whether or not that provider URL is the one we were + // passed as an argument. + bool correctURL = manifest.mProvider_urls[0] == aProvider.mConfigURL; + return IdentityCredential::ValidationPromise::CreateAndResolve( + correctURL, __func__); + }, + [](nsresult error) { + return IdentityCredential::ValidationPromise::CreateAndReject(error, + __func__); + }); +} + +// static +RefPtr +IdentityCredential::FetchInternalManifest( + nsIPrincipal* aPrincipal, const IdentityProviderConfig& aProvider) { + MOZ_ASSERT(XRE_IsParentProcess()); + // Build the URL + nsCString configLocation = aProvider.mConfigURL; + + // Create the global + RefPtr nullPrincipal = + NullPrincipal::CreateWithInheritedAttributes(aPrincipal); + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + nsCOMPtr global; + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + JS::Rooted sandbox(cx); + nsresult rv = xpc->CreateSandbox(cx, nullPrincipal, sandbox.address()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetManifestPromise::CreateAndReject(rv, + __func__); + } + MOZ_ASSERT(JS_IsGlobalObject(sandbox)); + global = xpc::NativeGlobal(sandbox); + if (NS_WARN_IF(!global)) { + return IdentityCredential::GetManifestPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + + // Create a new request + constexpr auto fragment = ""_ns; + auto internalRequest = + MakeSafeRefPtr(configLocation, fragment); + internalRequest->SetRedirectMode(RequestRedirect::Error); + internalRequest->SetCredentialsMode(RequestCredentials::Omit); + internalRequest->SetReferrerPolicy(ReferrerPolicy::No_referrer); + internalRequest->SetMode(RequestMode::Cors); + internalRequest->SetCacheMode(RequestCache::No_cache); + internalRequest->SetHeaders(new InternalHeaders(HeadersGuardEnum::Request)); + internalRequest->OverrideContentPolicyType( + nsContentPolicyType::TYPE_WEB_IDENTITY); + RefPtr request = + new Request(global, std::move(internalRequest), nullptr); + return FetchJSONStructure(request); +} + +// static +RefPtr +IdentityCredential::FetchAccountList( + nsIPrincipal* aPrincipal, const IdentityProviderConfig& aProvider, + const IdentityProviderAPIConfig& aManifest) { + MOZ_ASSERT(XRE_IsParentProcess()); + // Build the URL + nsCOMPtr baseURI; + nsresult rv = NS_NewURI(getter_AddRefs(baseURI), aProvider.mConfigURL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetAccountListPromise::CreateAndReject(rv, + __func__); + } + nsCOMPtr idpURI; + rv = NS_NewURI(getter_AddRefs(idpURI), aManifest.mAccounts_endpoint, nullptr, + baseURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetAccountListPromise::CreateAndReject(rv, + __func__); + } + nsCString configLocation; + rv = idpURI->GetSpec(configLocation); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetAccountListPromise::CreateAndReject(rv, + __func__); + } + + // Build the principal to use for this connection + // This is an expanded principal! It has the cookies of the IDP because it + // subsumes the constituent principals. It also has no serializable origin, + // so it won't send an Origin header even though this is a CORS mode + // request. It accomplishes this without being a SystemPrincipal too. + nsCOMPtr idpPrincipal = BasePrincipal::CreateContentPrincipal( + idpURI, aPrincipal->OriginAttributesRef()); + nsCOMPtr nullPrincipal = + NullPrincipal::CreateWithInheritedAttributes(aPrincipal); + AutoTArray, 2> allowList = {idpPrincipal, + nullPrincipal}; + RefPtr expandedPrincipal = + ExpandedPrincipal::Create(allowList, aPrincipal->OriginAttributesRef()); + + // Create the global + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + nsCOMPtr global; + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + JS::Rooted sandbox(cx); + rv = xpc->CreateSandbox(cx, expandedPrincipal, sandbox.address()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetAccountListPromise::CreateAndReject(rv, + __func__); + } + MOZ_ASSERT(JS_IsGlobalObject(sandbox)); + global = xpc::NativeGlobal(sandbox); + if (NS_WARN_IF(!global)) { + return IdentityCredential::GetAccountListPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + + // Create a new request + constexpr auto fragment = ""_ns; + auto internalRequest = + MakeSafeRefPtr(configLocation, fragment); + internalRequest->SetRedirectMode(RequestRedirect::Error); + internalRequest->SetCredentialsMode(RequestCredentials::Include); + internalRequest->SetReferrerPolicy(ReferrerPolicy::No_referrer); + internalRequest->SetMode(RequestMode::Cors); + internalRequest->SetCacheMode(RequestCache::No_cache); + internalRequest->SetHeaders(new InternalHeaders(HeadersGuardEnum::Request)); + internalRequest->OverrideContentPolicyType( + nsContentPolicyType::TYPE_WEB_IDENTITY); + RefPtr request = + new Request(global, std::move(internalRequest), nullptr); + + return FetchJSONStructure(request)->Then( + GetCurrentSerialEventTarget(), __func__, + [aManifest](const IdentityProviderAccountList& accountList) { + return IdentityCredential::GetAccountListPromise::CreateAndResolve( + std::make_tuple(aManifest, accountList), __func__); + }, + [](nsresult error) { + return IdentityCredential::GetAccountListPromise::CreateAndReject( + error, __func__); + }); +} + +// static +RefPtr IdentityCredential::FetchToken( + nsIPrincipal* aPrincipal, const IdentityProviderConfig& aProvider, + const IdentityProviderAPIConfig& aManifest, + const IdentityProviderAccount& aAccount) { + MOZ_ASSERT(XRE_IsParentProcess()); + // Build the URL + nsCOMPtr baseURI; + nsCString baseURIString = aProvider.mConfigURL; + nsresult rv = NS_NewURI(getter_AddRefs(baseURI), baseURIString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetTokenPromise::CreateAndReject(rv, __func__); + } + nsCOMPtr idpURI; + nsCString tokenSpec = aManifest.mId_assertion_endpoint; + rv = NS_NewURI(getter_AddRefs(idpURI), tokenSpec.get(), baseURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetTokenPromise::CreateAndReject(rv, __func__); + } + nsCString tokenLocation; + rv = idpURI->GetSpec(tokenLocation); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetTokenPromise::CreateAndReject(rv, __func__); + } + + // Create the global + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + nsCOMPtr global; + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + JS::Rooted sandbox(cx); + rv = xpc->CreateSandbox(cx, aPrincipal, sandbox.address()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetTokenPromise::CreateAndReject(rv, __func__); + } + MOZ_ASSERT(JS_IsGlobalObject(sandbox)); + global = xpc::NativeGlobal(sandbox); + if (NS_WARN_IF(!global)) { + return IdentityCredential::GetTokenPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + + // Create a new request + constexpr auto fragment = ""_ns; + auto internalRequest = + MakeSafeRefPtr(tokenLocation, fragment); + internalRequest->SetMethod("POST"_ns); + URLParams bodyValue; + bodyValue.Set(u"account_id"_ns, aAccount.mId); + bodyValue.Set(u"client_id"_ns, aProvider.mClientId); + if (aProvider.mNonce.WasPassed()) { + bodyValue.Set(u"nonce"_ns, aProvider.mNonce.Value()); + } + bodyValue.Set(u"disclosure_text_shown"_ns, u"false"_ns); + nsString bodyString; + bodyValue.Serialize(bodyString, true); + nsCString bodyCString = NS_ConvertUTF16toUTF8(bodyString); + nsCOMPtr streamBody; + rv = NS_NewCStringInputStream(getter_AddRefs(streamBody), bodyCString); + if (NS_FAILED(rv)) { + return IdentityCredential::GetTokenPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + + IgnoredErrorResult error; + RefPtr internalHeaders = + new InternalHeaders(HeadersGuardEnum::Request); + internalHeaders->Set("Content-Type"_ns, + "application/x-www-form-urlencoded"_ns, error); + if (NS_WARN_IF(error.Failed())) { + return IdentityCredential::GetTokenPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + internalRequest->SetHeaders(internalHeaders); + internalRequest->SetBody(streamBody, bodyCString.Length()); + internalRequest->SetRedirectMode(RequestRedirect::Error); + internalRequest->SetCredentialsMode(RequestCredentials::Include); + internalRequest->SetReferrerPolicy(ReferrerPolicy::Strict_origin); + internalRequest->SetMode(RequestMode::Cors); + internalRequest->SetCacheMode(RequestCache::No_cache); + internalRequest->OverrideContentPolicyType( + nsContentPolicyType::TYPE_WEB_IDENTITY); + RefPtr request = + new Request(global, std::move(internalRequest), nullptr); + return FetchJSONStructure(request)->Then( + GetCurrentSerialEventTarget(), __func__, + [aAccount](const IdentityProviderToken& token) { + return IdentityCredential::GetTokenPromise::CreateAndResolve( + std::make_tuple(token, aAccount), __func__); + }, + [](nsresult error) { + return IdentityCredential::GetTokenPromise::CreateAndReject(error, + __func__); + }); +} + +// static +RefPtr +IdentityCredential::FetchMetadata(nsIPrincipal* aPrincipal, + const IdentityProviderConfig& aProvider, + const IdentityProviderAPIConfig& aManifest) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(aPrincipal); + // Build the URL + nsCOMPtr baseURI; + nsCString baseURIString = aProvider.mConfigURL; + nsresult rv = NS_NewURI(getter_AddRefs(baseURI), baseURIString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetMetadataPromise::CreateAndReject(rv, + __func__); + } + nsCOMPtr idpURI; + nsCString metadataSpec = aManifest.mClient_metadata_endpoint; + rv = NS_NewURI(getter_AddRefs(idpURI), metadataSpec.get(), baseURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetMetadataPromise::CreateAndReject(rv, + __func__); + } + nsCString configLocation; + rv = idpURI->GetSpec(configLocation); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetMetadataPromise::CreateAndReject(rv, + __func__); + } + + // Create the global + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + nsCOMPtr global; + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + JS::Rooted sandbox(cx); + rv = xpc->CreateSandbox(cx, aPrincipal, sandbox.address()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetMetadataPromise::CreateAndReject(rv, + __func__); + } + MOZ_ASSERT(JS_IsGlobalObject(sandbox)); + global = xpc::NativeGlobal(sandbox); + if (NS_WARN_IF(!global)) { + return IdentityCredential::GetMetadataPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + + // Create a new request + constexpr auto fragment = ""_ns; + auto internalRequest = + MakeSafeRefPtr(configLocation, fragment); + internalRequest->SetRedirectMode(RequestRedirect::Error); + internalRequest->SetCredentialsMode(RequestCredentials::Omit); + internalRequest->SetReferrerPolicy(ReferrerPolicy::No_referrer); + internalRequest->SetMode(RequestMode::Cors); + internalRequest->SetCacheMode(RequestCache::No_cache); + internalRequest->SetHeaders(new InternalHeaders(HeadersGuardEnum::Request)); + internalRequest->OverrideContentPolicyType( + nsContentPolicyType::TYPE_WEB_IDENTITY); + RefPtr request = + new Request(global, std::move(internalRequest), nullptr); + return FetchJSONStructure(request); +} + +// static +RefPtr +IdentityCredential::PromptUserToSelectProvider( + BrowsingContext* aBrowsingContext, + const Sequence& aProviders, + const Sequence& aManifests) { + MOZ_ASSERT(aBrowsingContext); + RefPtr< + IdentityCredential::GetIdentityProviderConfigWithManifestPromise::Private> + resultPromise = new IdentityCredential:: + GetIdentityProviderConfigWithManifestPromise::Private(__func__); + + if (NS_WARN_IF(!aBrowsingContext)) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + + nsresult error; + nsCOMPtr icPromptService = + mozilla::components::IdentityCredentialPromptService::Service(&error); + if (NS_WARN_IF(!icPromptService)) { + resultPromise->Reject(error, __func__); + return resultPromise; + } + + nsCOMPtr wrapped = do_QueryInterface(icPromptService); + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(wrapped->GetJSObjectGlobal()))) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + + JS::Rooted providersJS(jsapi.cx()); + bool success = ToJSValue(jsapi.cx(), aProviders, &providersJS); + if (NS_WARN_IF(!success)) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + + // Convert each settled MozPromise into a Nullable + Sequence> manifests; + for (GetManifestPromise::ResolveOrRejectValue manifest : aManifests) { + if (manifest.IsResolve()) { + if (NS_WARN_IF( + !manifests.AppendElement(manifest.ResolveValue(), fallible))) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + } else { + if (NS_WARN_IF(!manifests.AppendElement( + Nullable(), fallible))) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + } + } + JS::Rooted manifestsJS(jsapi.cx()); + success = ToJSValue(jsapi.cx(), manifests, &manifestsJS); + if (NS_WARN_IF(!success)) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + + RefPtr showPromptPromise; + icPromptService->ShowProviderPrompt(aBrowsingContext, providersJS, + manifestsJS, + getter_AddRefs(showPromptPromise)); + + showPromptPromise->AddCallbacksWithCycleCollectedArgs( + [aProviders, aManifests, resultPromise]( + JSContext*, JS::Handle aValue, ErrorResult&) { + int32_t result = aValue.toInt32(); + if (result < 0 || (uint32_t)result > aProviders.Length() || + (uint32_t)result > aManifests.Length()) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return; + } + const IdentityProviderConfig& resolvedProvider = + aProviders.ElementAt(result); + if (!aManifests.ElementAt(result).IsResolve()) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return; + } + const IdentityProviderAPIConfig& resolvedManifest = + aManifests.ElementAt(result).ResolveValue(); + resultPromise->Resolve( + std::make_tuple(resolvedProvider, resolvedManifest), __func__); + }, + [resultPromise](JSContext*, JS::Handle aValue, ErrorResult&) { + resultPromise->Reject( + Promise::TryExtractNSResultFromRejectionValue(aValue), __func__); + }); + // Working around https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85883 + showPromptPromise->AppendNativeHandler( + new MozPromiseRejectOnDestruction{resultPromise, __func__}); + + return resultPromise; +} + +// static +RefPtr +IdentityCredential::PromptUserToSelectAccount( + BrowsingContext* aBrowsingContext, + const IdentityProviderAccountList& aAccounts, + const IdentityProviderConfig& aProvider, + const IdentityProviderAPIConfig& aManifest) { + MOZ_ASSERT(aBrowsingContext); + RefPtr resultPromise = + new IdentityCredential::GetAccountPromise::Private(__func__); + + if (NS_WARN_IF(!aBrowsingContext)) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + + nsresult error; + nsCOMPtr icPromptService = + mozilla::components::IdentityCredentialPromptService::Service(&error); + if (NS_WARN_IF(!icPromptService)) { + resultPromise->Reject(error, __func__); + return resultPromise; + } + + nsCOMPtr wrapped = do_QueryInterface(icPromptService); + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(wrapped->GetJSObjectGlobal()))) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + + JS::Rooted accountsJS(jsapi.cx()); + bool success = ToJSValue(jsapi.cx(), aAccounts, &accountsJS); + if (NS_WARN_IF(!success)) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + + JS::Rooted providerJS(jsapi.cx()); + success = ToJSValue(jsapi.cx(), aProvider, &providerJS); + if (NS_WARN_IF(!success)) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + + JS::Rooted manifestJS(jsapi.cx()); + success = ToJSValue(jsapi.cx(), aManifest, &manifestJS); + if (NS_WARN_IF(!success)) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + + RefPtr showPromptPromise; + icPromptService->ShowAccountListPrompt(aBrowsingContext, accountsJS, + providerJS, manifestJS, + getter_AddRefs(showPromptPromise)); + + showPromptPromise->AddCallbacksWithCycleCollectedArgs( + [aAccounts, resultPromise, aManifest]( + JSContext*, JS::Handle aValue, ErrorResult&) { + int32_t result = aValue.toInt32(); + if (!aAccounts.mAccounts.WasPassed() || result < 0 || + (uint32_t)result > aAccounts.mAccounts.Value().Length()) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return; + } + const IdentityProviderAccount& resolved = + aAccounts.mAccounts.Value().ElementAt(result); + resultPromise->Resolve(std::make_tuple(aManifest, resolved), __func__); + }, + [resultPromise](JSContext*, JS::Handle aValue, ErrorResult&) { + resultPromise->Reject( + Promise::TryExtractNSResultFromRejectionValue(aValue), __func__); + }); + // Working around https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85883 + showPromptPromise->AppendNativeHandler( + new MozPromiseRejectOnDestruction{resultPromise, __func__}); + + return resultPromise; +} + +// static +RefPtr +IdentityCredential::PromptUserWithPolicy( + BrowsingContext* aBrowsingContext, nsIPrincipal* aPrincipal, + const IdentityProviderAccount& aAccount, + const IdentityProviderAPIConfig& aManifest, + const IdentityProviderConfig& aProvider) { + MOZ_ASSERT(aBrowsingContext); + MOZ_ASSERT(aPrincipal); + + nsresult error; + nsCOMPtr icStorageService = + mozilla::components::IdentityCredentialStorageService::Service(&error); + if (NS_WARN_IF(!icStorageService)) { + return IdentityCredential::GetAccountPromise::CreateAndReject(error, + __func__); + } + + // Check the storage bit + nsCString configLocation = aProvider.mConfigURL; + nsCOMPtr idpURI; + error = NS_NewURI(getter_AddRefs(idpURI), configLocation); + if (NS_WARN_IF(NS_FAILED(error))) { + return IdentityCredential::GetAccountPromise::CreateAndReject(error, + __func__); + } + bool registered = false; + bool allowLogout = false; + nsCOMPtr idpPrincipal = BasePrincipal::CreateContentPrincipal( + idpURI, aPrincipal->OriginAttributesRef()); + error = icStorageService->GetState(aPrincipal, idpPrincipal, + NS_ConvertUTF16toUTF8(aAccount.mId), + ®istered, &allowLogout); + if (NS_WARN_IF(NS_FAILED(error))) { + return IdentityCredential::GetAccountPromise::CreateAndReject(error, + __func__); + } + + // if registered, mark as logged in and return + if (registered) { + icStorageService->SetState(aPrincipal, idpPrincipal, + NS_ConvertUTF16toUTF8(aAccount.mId), true, true); + return IdentityCredential::GetAccountPromise::CreateAndResolve( + std::make_tuple(aManifest, aAccount), __func__); + } + + // otherwise, fetch ->Then display ->Then return ->Catch reject + RefPtr browsingContext(aBrowsingContext); + nsCOMPtr argumentPrincipal(aPrincipal); + return FetchMetadata(aPrincipal, aProvider, aManifest) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [aAccount, aManifest, aProvider, argumentPrincipal, browsingContext, + icStorageService, + idpPrincipal](const IdentityProviderClientMetadata& metadata) + -> RefPtr { + nsresult error; + nsCOMPtr icPromptService = + mozilla::components::IdentityCredentialPromptService::Service( + &error); + if (NS_WARN_IF(!icPromptService)) { + return GenericPromise::CreateAndReject(error, __func__); + } + nsCOMPtr wrapped = + do_QueryInterface(icPromptService); + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(wrapped->GetJSObjectGlobal()))) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, + __func__); + } + + JS::Rooted providerJS(jsapi.cx()); + bool success = ToJSValue(jsapi.cx(), aProvider, &providerJS); + if (NS_WARN_IF(!success)) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, + __func__); + } + JS::Rooted metadataJS(jsapi.cx()); + success = ToJSValue(jsapi.cx(), metadata, &metadataJS); + if (NS_WARN_IF(!success)) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, + __func__); + } + JS::Rooted manifestJS(jsapi.cx()); + success = ToJSValue(jsapi.cx(), aManifest, &manifestJS); + if (NS_WARN_IF(!success)) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, + __func__); + } + + RefPtr showPromptPromise; + icPromptService->ShowPolicyPrompt( + browsingContext, providerJS, manifestJS, metadataJS, + getter_AddRefs(showPromptPromise)); + + RefPtr resultPromise = + new GenericPromise::Private(__func__); + showPromptPromise->AddCallbacksWithCycleCollectedArgs( + [aAccount, argumentPrincipal, idpPrincipal, resultPromise, + icStorageService](JSContext* aCx, JS::Handle aValue, + ErrorResult&) { + bool isBool = aValue.isBoolean(); + if (!isBool) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return; + } + icStorageService->SetState( + argumentPrincipal, idpPrincipal, + NS_ConvertUTF16toUTF8(aAccount.mId), true, true); + resultPromise->Resolve(aValue.toBoolean(), __func__); + }, + [resultPromise](JSContext*, JS::Handle aValue, + ErrorResult&) { + resultPromise->Reject( + Promise::TryExtractNSResultFromRejectionValue(aValue), + __func__); + }); + // Working around https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85883 + showPromptPromise->AppendNativeHandler( + new MozPromiseRejectOnDestruction{resultPromise, __func__}); + return resultPromise; + }, + [](nsresult error) { + return GenericPromise::CreateAndReject(error, __func__); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [aManifest, aAccount](bool success) { + if (success) { + return IdentityCredential::GetAccountPromise::CreateAndResolve( + std::make_tuple(aManifest, aAccount), __func__); + } + return IdentityCredential::GetAccountPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + }, + [](nsresult error) { + return IdentityCredential::GetAccountPromise::CreateAndReject( + error, __func__); + }); +} + +// static +void IdentityCredential::CloseUserInterface(BrowsingContext* aBrowsingContext) { + nsresult error; + nsCOMPtr icPromptService = + mozilla::components::IdentityCredentialPromptService::Service(&error); + if (NS_WARN_IF(!icPromptService)) { + return; + } + icPromptService->Close(aBrowsingContext); +} + +// static +already_AddRefed IdentityCredential::LogoutRPs( + GlobalObject& aGlobal, + const Sequence& aLogoutRequests, + ErrorResult& aRv) { + nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr promise = Promise::CreateResolvedWithUndefined(global, aRv); + NS_ENSURE_FALSE(aRv.Failed(), nullptr); + nsresult rv; + nsCOMPtr icStorageService = + components::IdentityCredentialStorageService::Service(&rv); + if (NS_WARN_IF(!icStorageService)) { + aRv.Throw(rv); + return nullptr; + } + + RefPtr rpPrincipal = global->PrincipalOrNull(); + for (const auto& request : aLogoutRequests) { + // Get the current state + nsCOMPtr idpURI; + rv = NS_NewURI(getter_AddRefs(idpURI), request.mUrl); + if (NS_FAILED(rv)) { + aRv.ThrowTypeError(request.mUrl); + return nullptr; + } + nsCOMPtr idpPrincipal = BasePrincipal::CreateContentPrincipal( + idpURI, rpPrincipal->OriginAttributesRef()); + bool registered, allowLogout; + icStorageService->GetState(rpPrincipal, idpPrincipal, request.mAccountId, + ®istered, &allowLogout); + + // Ignore this request if it isn't permitted + if (!(registered && allowLogout)) { + continue; + } + + // Issue the logout request + constexpr auto fragment = ""_ns; + auto internalRequest = + MakeSafeRefPtr(request.mUrl, fragment); + internalRequest->SetRedirectMode(RequestRedirect::Error); + internalRequest->SetCredentialsMode(RequestCredentials::Include); + internalRequest->SetReferrerPolicy(ReferrerPolicy::Strict_origin); + internalRequest->SetMode(RequestMode::Cors); + internalRequest->SetCacheMode(RequestCache::No_cache); + internalRequest->OverrideContentPolicyType( + nsContentPolicyType::TYPE_WEB_IDENTITY); + RefPtr domRequest = + new Request(global, std::move(internalRequest), nullptr); + RequestOrUSVString fetchInput; + fetchInput.SetAsRequest() = domRequest; + RootedDictionary requestInit(RootingCx()); + IgnoredErrorResult error; + RefPtr fetchPromise = FetchRequest(global, fetchInput, requestInit, + CallerType::System, error); + + // Change state to disallow more logout requests + icStorageService->SetState(rpPrincipal, idpPrincipal, request.mAccountId, + true, false); + } + return promise.forget(); +} + +} // namespace mozilla::dom diff --git a/dom/credentialmanagement/identity/IdentityCredential.h b/dom/credentialmanagement/identity/IdentityCredential.h new file mode 100644 index 0000000000..598a8bf99c --- /dev/null +++ b/dom/credentialmanagement/identity/IdentityCredential.h @@ -0,0 +1,314 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_IdentityCredential_h +#define mozilla_dom_IdentityCredential_h + +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/Credential.h" +#include "mozilla/dom/IPCIdentityCredential.h" +#include "mozilla/MozPromise.h" + +namespace mozilla::dom { + +// This is the primary starting point for FedCM in the platform. +// This class is the implementation of the IdentityCredential object +// that is the value returned from the navigator.credentials.get call +// with an "identity" argument. It also includes static functions that +// perform operations that are used in constructing the credential. +class IdentityCredential final : public Credential { + public: + // These are promise types, all used to support the async implementation of + // this API. All are of the form MozPromise, nsresult>. + // Tuples are included to shuffle additional values along, so that the + // intermediate state is entirely in the promise chain and we don't have to + // capture an early step's result into a callback for a subsequent promise. + typedef MozPromise, nsresult, true> + GetIdentityCredentialPromise; + typedef MozPromise + GetIPCIdentityCredentialPromise; + typedef MozPromise + GetIdentityProviderConfigPromise; + typedef MozPromise ValidationPromise; + typedef MozPromise + GetManifestPromise; + typedef std::tuple + IdentityProviderConfigWithManifest; + typedef MozPromise + GetIdentityProviderConfigWithManifestPromise; + typedef MozPromise< + std::tuple, + nsresult, true> + GetAccountListPromise; + typedef MozPromise, + nsresult, true> + GetTokenPromise; + typedef MozPromise< + std::tuple, nsresult, + true> + GetAccountPromise; + typedef MozPromise + GetMetadataPromise; + + // This needs to be constructed in the context of a window + explicit IdentityCredential(nsPIDOMWindowInner* aParent); + + protected: + ~IdentityCredential() override; + + public: + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + // This builds a value from an IPC-friendly version. This type is returned + // to the caller of navigator.credentials.get, however we get an IPC friendly + // version back from the main process to the content process. + // This is a deep copy of the token, ID, and type. + void CopyValuesFrom(const IPCIdentityCredential& aOther); + + // This is the inverse of CopyValuesFrom. Included for completeness. + IPCIdentityCredential MakeIPCIdentityCredential(); + + // Getter and setter for the token member of this class + void GetToken(nsAString& aToken) const; + void SetToken(const nsAString& aToken); + + // This function allows a relying party to send one last credentialed request + // to the IDP when logging out. This only works if the current account state + // in the IdentityCredentialStorageService allows logouts and clears that bit + // when a request is sent. + // + // Arguments: + // aGlobal: the global of the window calling this function + // aLogoutRequest: all of the logout requests to try to send. + // This is pairs of the IDP's logout url and the account + // ID for that IDP. + // Return value: + // a promise resolving to undefined + // Side effects: + // Will send a network request to each IDP that have a state allowing + // logouts and disables that bit. + static already_AddRefed LogoutRPs( + GlobalObject& aGlobal, + const Sequence& aLogoutRequests, + ErrorResult& aRv); + + // This is the main static function called when a credential needs to be + // fetched from the IDP. Called in the content process. + // This is mostly a passthrough to `DiscoverFromExternalSourceInMainProcess`. + static RefPtr DiscoverFromExternalSource( + nsPIDOMWindowInner* aParent, const CredentialRequestOptions& aOptions, + bool aSameOriginWithAncestors); + + // Start the FedCM flow. This will start the timeout timer, fire initial + // network requests, prompt the user, and call into CreateCredential. + // + // Arguments: + // aPrincipal: the caller of navigator.credentials.get()'s principal + // aBrowsingContext: the BC of the caller of navigator.credentials.get() + // aOptions: argument passed to navigator.credentials.get() + // Return value: + // a promise resolving to an IPC credential with type "identity", id + // constructed to identify it, and token corresponding to the token + // fetched in FetchToken. This promise may reject with nsresult errors. + // Side effects: + // Will send network requests to the IDP. The details of which are in the + // other static methods here. + static RefPtr + DiscoverFromExternalSourceInMainProcess( + nsIPrincipal* aPrincipal, CanonicalBrowsingContext* aBrowsingContext, + const IdentityCredentialRequestOptions& aOptions); + + // Create an IPC credential that can be passed back to the content process. + // This calls a lot of helpers to do the logic of going from a single provider + // to a bearer token for an account at that provider. + // + // Arguments: + // aPrincipal: the caller of navigator.credentials.get()'s principal + // aBrowsingContext: the BC of the caller of navigator.credentials.get() + // aProvider: the provider to validate the root manifest of + // aManifest: the internal manifest of the identity provider + // Return value: + // a promise resolving to an IPC credential with type "identity", id + // constructed to identify it, and token corresponding to the token + // fetched in FetchToken. This promise may reject with nsresult errors. + // Side effects: + // Will send network requests to the IDP. The details of which are in the + // other static methods here. + static RefPtr CreateCredential( + nsIPrincipal* aPrincipal, BrowsingContext* aBrowsingContext, + const IdentityProviderConfig& aProvider, + const IdentityProviderAPIConfig& aManifest); + + // Performs a Fetch for the root manifest of the provided identity provider + // and validates it as correct. The returned promise resolves with a bool + // that is true if everything is valid. + // + // Arguments: + // aPrincipal: the caller of navigator.credentials.get()'s principal + // aProvider: the provider to validate the root manifest of + // Return value: + // promise that resolves to a bool that indicates success. Will reject + // when there are network or other errors. + // Side effects: + // Network request to the IDP's well-known from inside a NullPrincipal + // sandbox + // + static RefPtr CheckRootManifest( + nsIPrincipal* aPrincipal, const IdentityProviderConfig& aProvider); + + // Performs a Fetch for the internal manifest of the provided identity + // provider. The returned promise resolves with the manifest retrieved. + // + // Arguments: + // aPrincipal: the caller of navigator.credentials.get()'s principal + // aProvider: the provider to fetch the root manifest + // Return value: + // promise that resolves to the internal manifest. Will reject + // when there are network or other errors. + // Side effects: + // Network request to the URL in aProvider as the manifest from inside a + // NullPrincipal sandbox + // + static RefPtr FetchInternalManifest( + nsIPrincipal* aPrincipal, const IdentityProviderConfig& aProvider); + + // Performs a Fetch for the account list from the provided identity + // provider. The returned promise resolves with the manifest and the fetched + // account list in a tuple of objects. We put the argument manifest in the + // tuple to facilitate clean promise chaining. + // + // Arguments: + // aPrincipal: the caller of navigator.credentials.get()'s principal + // aProvider: the provider to get account lists from + // aManifest: the provider's internal manifest + // Return value: + // promise that resolves to a Tuple of the passed manifest and the fetched + // account list. Will reject when there are network or other errors. + // Side effects: + // Network request to the provider supplied account endpoint with + // credentials but without any indication of aPrincipal. + // + static RefPtr FetchAccountList( + nsIPrincipal* aPrincipal, const IdentityProviderConfig& aProvider, + const IdentityProviderAPIConfig& aManifest); + + // Performs a Fetch for a bearer token to the provided identity + // provider for a given account. The returned promise resolves with the + // account argument and the fetched token in a tuple of objects. + // We put the argument account in the + // tuple to facilitate clean promise chaining. + // + // Arguments: + // aPrincipal: the caller of navigator.credentials.get()'s principal + // aProvider: the provider to get account lists from + // aManifest: the provider's internal manifest + // aAccount: the account to request + // Return value: + // promise that resolves to a Tuple of the passed account and the fetched + // token. Will reject when there are network or other errors. + // Side effects: + // Network request to the provider supplied token endpoint with + // credentials and including information about the requesting principal. + // + static RefPtr FetchToken( + nsIPrincipal* aPrincipal, const IdentityProviderConfig& aProvider, + const IdentityProviderAPIConfig& aManifest, + const IdentityProviderAccount& aAccount); + + // Performs a Fetch for links to legal info about the identity provider. + // The returned promise resolves with the information in an object. + // + // Arguments: + // aPrincipal: the caller of navigator.credentials.get()'s principal + // aProvider: the identity provider to get information from + // aManfiest: the identity provider's manifest + // Return value: + // promise that resolves with an object containing legal information for + // aProvider + // Side effects: + // Network request to the provider supplied token endpoint with + // credentials and including information about the requesting principal. + // + static RefPtr FetchMetadata( + nsIPrincipal* aPrincipal, const IdentityProviderConfig& aProvider, + const IdentityProviderAPIConfig& aManifest); + + // Show the user a dialog to select what identity provider they would like + // to try to log in with. + // + // Arguments: + // aBrowsingContext: the BC of the caller of navigator.credentials.get() + // aProviders: the providers to let the user select from + // aManifests: the manifests + // Return value: + // a promise resolving to an identity provider that the user took action + // to select. This promise may reject with nsresult errors. + // Side effects: + // Will show a dialog to the user. + static RefPtr + PromptUserToSelectProvider( + BrowsingContext* aBrowsingContext, + const Sequence& aProviders, + const Sequence& aManifests); + + // Show the user a dialog to select what account they would like + // to try to log in with. + // + // Arguments: + // aBrowsingContext: the BC of the caller of navigator.credentials.get() + // aAccounts: the accounts to let the user select from + // aProvider: the provider that was chosen + // aManifest: the identity provider that was chosen's manifest + // Return value: + // a promise resolving to an account that the user took action + // to select (and aManifest). This promise may reject with nsresult errors. + // Side effects: + // Will show a dialog to the user. + static RefPtr PromptUserToSelectAccount( + BrowsingContext* aBrowsingContext, + const IdentityProviderAccountList& aAccounts, + const IdentityProviderConfig& aProvider, + const IdentityProviderAPIConfig& aManifest); + + // Show the user a dialog to select what account they would like + // to try to log in with. + // + // Arguments: + // aBrowsingContext: the BC of the caller of navigator.credentials.get() + // aAccount: the accounts the user chose + // aManifest: the identity provider that was chosen's manifest + // aProvider: the identity provider that was chosen + // Return value: + // a promise resolving to an account that the user agreed to use (and + // aManifest). This promise may reject with nsresult errors. This includes + // if the user denied the terms and privacy policy + // Side effects: + // Will show a dialog to the user. Will send a network request to the + // identity provider. Modifies the IdentityCredentialStorageService state + // for this account. + static RefPtr PromptUserWithPolicy( + BrowsingContext* aBrowsingContext, nsIPrincipal* aPrincipal, + const IdentityProviderAccount& aAccount, + const IdentityProviderAPIConfig& aManifest, + const IdentityProviderConfig& aProvider); + + // Close all dialogs associated with IdentityCredential generation on the + // provided browsing context + // + // Arguments: + // aBrowsingContext: the BC of the caller of navigator.credentials.get() + // Side effects: + // Will close a dialog shown to the user. + static void CloseUserInterface(BrowsingContext* aBrowsingContext); + + private: + nsAutoString mToken; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_IdentityCredential_h diff --git a/dom/credentialmanagement/identity/IdentityCredentialSerializationHelpers.h b/dom/credentialmanagement/identity/IdentityCredentialSerializationHelpers.h new file mode 100644 index 0000000000..f98773ef85 --- /dev/null +++ b/dom/credentialmanagement/identity/IdentityCredentialSerializationHelpers.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_identitycredentialserializationhelpers_h__ +#define mozilla_dom_identitycredentialserializationhelpers_h__ + +#include "mozilla/dom/IdentityCredential.h" +#include "mozilla/dom/IdentityCredentialBinding.h" + +namespace IPC { + +template <> +struct ParamTraits { + typedef mozilla::dom::IdentityProviderConfig paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mConfigURL); + WriteParam(aWriter, aParam.mClientId); + WriteParam(aWriter, aParam.mNonce); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return ReadParam(aReader, &aResult->mConfigURL) && + ReadParam(aReader, &aResult->mClientId) && + ReadParam(aReader, &aResult->mNonce); + } +}; + +template <> +struct ParamTraits { + typedef mozilla::dom::IdentityCredentialRequestOptions paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mProviders); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return ReadParam(aReader, &aResult->mProviders); + ; + } +}; + +} // namespace IPC + +#endif // mozilla_dom_identitycredentialserializationhelpers_h__ diff --git a/dom/credentialmanagement/identity/IdentityNetworkHelpers.h b/dom/credentialmanagement/identity/IdentityNetworkHelpers.h new file mode 100644 index 0000000000..d812269612 --- /dev/null +++ b/dom/credentialmanagement/identity/IdentityNetworkHelpers.h @@ -0,0 +1,113 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_IdentityNetworkHelpers_h +#define mozilla_dom_IdentityNetworkHelpers_h + +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/Promise-inl.h" +#include "mozilla/dom/Request.h" +#include "mozilla/dom/Response.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/MozPromise.h" + +namespace mozilla::dom { + +// Helper to get a JSON structure via a Fetch. +// The Request must already be built and T should be a webidl type with +// annotation GenerateConversionToJS so it has an Init method. +template > +RefPtr FetchJSONStructure(Request* aRequest) { + MOZ_ASSERT(XRE_IsParentProcess()); + + // Create the returned Promise + RefPtr resultPromise = + new typename TPromise::Private(__func__); + + // Fetch the provided request + RequestOrUSVString fetchInput; + fetchInput.SetAsRequest() = aRequest; + RootedDictionary requestInit(RootingCx()); + IgnoredErrorResult error; + RefPtr fetchPromise = + FetchRequest(aRequest->GetParentObject(), fetchInput, requestInit, + CallerType::System, error); + if (NS_WARN_IF(error.Failed())) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + + // Working around https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85883 + RefPtr reject = + new MozPromiseRejectOnDestruction{resultPromise, __func__}; + + // Handle the response + fetchPromise->AddCallbacksWithCycleCollectedArgs( + [resultPromise, reject](JSContext* aCx, JS::Handle aValue, + ErrorResult&) { + // Get the Response object from the argument to the callback + if (NS_WARN_IF(!aValue.isObject())) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return; + } + JS::Rooted obj(aCx, &aValue.toObject()); + MOZ_ASSERT(obj); + Response* response = nullptr; + if (NS_WARN_IF(NS_FAILED(UNWRAP_OBJECT(Response, &obj, response)))) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return; + } + + // Make sure the request was a success + if (!response->Ok()) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return; + } + + // Parse the body into JSON, which must be done async + IgnoredErrorResult error; + RefPtr jsonPromise = response->ConsumeBody( + aCx, BodyConsumer::ConsumeType::CONSUME_JSON, error); + if (NS_WARN_IF(error.Failed())) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return; + } + + // Handle the parsed JSON from the Response body + jsonPromise->AddCallbacksWithCycleCollectedArgs( + [resultPromise](JSContext* aCx, JS::Handle aValue, + ErrorResult&) { + // Parse the JSON into the correct type, validating fields and + // types + T result; + bool success = result.Init(aCx, aValue); + if (!success) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return; + } + + resultPromise->Resolve(result, __func__); + }, + [resultPromise](JSContext*, JS::Handle aValue, + ErrorResult&) { + resultPromise->Reject( + Promise::TryExtractNSResultFromRejectionValue(aValue), + __func__); + }); + jsonPromise->AppendNativeHandler(reject); + }, + [resultPromise](JSContext*, JS::Handle aValue, ErrorResult&) { + resultPromise->Reject( + Promise::TryExtractNSResultFromRejectionValue(aValue), __func__); + }); + fetchPromise->AppendNativeHandler(reject); + + return resultPromise; +} + +} // namespace mozilla::dom + +#endif // mozilla_dom_IdentityNetworkHelpers_h diff --git a/dom/credentialmanagement/identity/moz.build b/dom/credentialmanagement/identity/moz.build new file mode 100644 index 0000000000..f7a053d676 --- /dev/null +++ b/dom/credentialmanagement/identity/moz.build @@ -0,0 +1,28 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Credential Management") + +EXPORTS.mozilla.dom += [ + "IdentityCredential.h", + "IdentityCredentialSerializationHelpers.h", + "IdentityNetworkHelpers.h", +] + +IPDL_SOURCES += [ + "IPCIdentityCredential.ipdlh", +] + +UNIFIED_SOURCES += ["IdentityCredential.cpp"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"] +MOCHITEST_MANIFESTS += ["tests/mochitest/mochitest.toml"] diff --git a/dom/credentialmanagement/identity/tests/browser/browser.toml b/dom/credentialmanagement/identity/tests/browser/browser.toml new file mode 100644 index 0000000000..431d5a8a01 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/browser/browser.toml @@ -0,0 +1,21 @@ +[DEFAULT] +prefs = [ + "dom.security.credentialmanagement.identity.enabled=true", + "dom.security.credentialmanagement.identity.ignore_well_known=true", + "privacy.antitracking.enableWebcompat=false", # disables opener heuristic +] +scheme = "https" +support-files = [ + "server_accounts.json", + "server_accounts.json^headers^", + "server_idtoken.json", + "server_idtoken.json^headers^", + "server_manifest.json", + "server_manifest.json^headers^", + "server_metadata.json", + "server_metadata.json^headers^", +] + +["browser_close_prompt_on_timeout.js"] + +["browser_single_concurrent_identity_request.js"] diff --git a/dom/credentialmanagement/identity/tests/browser/browser_close_prompt_on_timeout.js b/dom/credentialmanagement/identity/tests/browser/browser_close_prompt_on_timeout.js new file mode 100644 index 0000000000..a71747e842 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/browser/browser_close_prompt_on_timeout.js @@ -0,0 +1,63 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URL = "https://example.com/"; + +add_task(async function test_close_prompt_on_timeout() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "dom.security.credentialmanagement.identity.reject_delay.duration_ms", + 1000, + ], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + let requestCredential = async function () { + let promise = content.navigator.credentials.get({ + identity: { + providers: [ + { + configURL: + "https://example.net/tests/dom/credentialmanagement/identity/tests/browser/server_manifest.json", + clientId: "browser", + nonce: "nonce", + }, + ], + }, + }); + try { + return await promise; + } catch (err) { + return err; + } + }; + + let popupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + + let request = ContentTask.spawn(tab.linkedBrowser, null, requestCredential); + + await popupShown; + await request; + + let notification = PopupNotifications.getNotification( + "identity-credential", + tab.linkedBrowser + ); + ok( + !notification, + "Identity Credential notification must not be present after timeout." + ); + + // Close tabs. + await BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); diff --git a/dom/credentialmanagement/identity/tests/browser/browser_single_concurrent_identity_request.js b/dom/credentialmanagement/identity/tests/browser/browser_single_concurrent_identity_request.js new file mode 100644 index 0000000000..2c3d91e521 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/browser/browser_single_concurrent_identity_request.js @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URL = "https://example.com/"; + +add_task(async function test_concurrent_identity_credential() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + let requestCredential = async function () { + let promise = content.navigator.credentials.get({ + identity: { + providers: [ + { + configURL: + "https://example.net/tests/dom/credentialmanagement/identity/tests/browser/server_manifest.json", + clientId: "browser", + nonce: "nonce", + }, + ], + }, + }); + try { + return await promise; + } catch (err) { + return err; + } + }; + + ContentTask.spawn(tab.linkedBrowser, null, requestCredential); + + let secondRequest = ContentTask.spawn( + tab.linkedBrowser, + null, + requestCredential + ); + + let concurrentResponse = await secondRequest; + ok(concurrentResponse, "expect a result from the second request."); + ok(concurrentResponse.name, "expect a DOMException which must have a name."); + is( + concurrentResponse.name, + "InvalidStateError", + "Expected 'InvalidStateError', but got '" + concurrentResponse.name + "'" + ); + + // Close tabs. + await BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/credentialmanagement/identity/tests/browser/server_accounts.json b/dom/credentialmanagement/identity/tests/browser/server_accounts.json new file mode 100644 index 0000000000..90e463584f --- /dev/null +++ b/dom/credentialmanagement/identity/tests/browser/server_accounts.json @@ -0,0 +1,12 @@ +{ + "accounts": [ + { + "id": "1234", + "given_name": "John", + "name": "John Doe", + "email": "john_doe@idp.example", + "picture": "https://idp.example/profile/123", + "approved_clients": ["123", "456", "789"] + } + ] +} diff --git a/dom/credentialmanagement/identity/tests/browser/server_accounts.json^headers^ b/dom/credentialmanagement/identity/tests/browser/server_accounts.json^headers^ new file mode 100644 index 0000000000..313fe12921 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/browser/server_accounts.json^headers^ @@ -0,0 +1,3 @@ +Content-Type: application/json +Access-Control-Allow-Origin: * +Access-Control-Allow-Credentials: true \ No newline at end of file diff --git a/dom/credentialmanagement/identity/tests/browser/server_idtoken.json b/dom/credentialmanagement/identity/tests/browser/server_idtoken.json new file mode 100644 index 0000000000..cd1840b349 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/browser/server_idtoken.json @@ -0,0 +1 @@ +{ "token": "result" } diff --git a/dom/credentialmanagement/identity/tests/browser/server_idtoken.json^headers^ b/dom/credentialmanagement/identity/tests/browser/server_idtoken.json^headers^ new file mode 100644 index 0000000000..0b4f8505f2 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/browser/server_idtoken.json^headers^ @@ -0,0 +1,3 @@ +Content-Type: application/json +Access-Control-Allow-Origin: https://example.com +Access-Control-Allow-Credentials: true diff --git a/dom/credentialmanagement/identity/tests/browser/server_manifest.json b/dom/credentialmanagement/identity/tests/browser/server_manifest.json new file mode 100644 index 0000000000..349ae5787b --- /dev/null +++ b/dom/credentialmanagement/identity/tests/browser/server_manifest.json @@ -0,0 +1,5 @@ +{ + "accounts_endpoint": "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_accounts.json", + "client_metadata_endpoint": "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_metadata.json", + "id_assertion_endpoint": "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_idtoken.json" +} diff --git a/dom/credentialmanagement/identity/tests/browser/server_manifest.json^headers^ b/dom/credentialmanagement/identity/tests/browser/server_manifest.json^headers^ new file mode 100644 index 0000000000..75875a7cf3 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/browser/server_manifest.json^headers^ @@ -0,0 +1,2 @@ +Content-Type: application/json +Access-Control-Allow-Origin: * diff --git a/dom/credentialmanagement/identity/tests/browser/server_metadata.json b/dom/credentialmanagement/identity/tests/browser/server_metadata.json new file mode 100644 index 0000000000..1e16c942b5 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/browser/server_metadata.json @@ -0,0 +1,4 @@ +{ + "privacy_policy_url": "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/null.txt", + "terms_of_service_url": "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/null.txt" +} diff --git a/dom/credentialmanagement/identity/tests/browser/server_metadata.json^headers^ b/dom/credentialmanagement/identity/tests/browser/server_metadata.json^headers^ new file mode 100644 index 0000000000..75875a7cf3 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/browser/server_metadata.json^headers^ @@ -0,0 +1,2 @@ +Content-Type: application/json +Access-Control-Allow-Origin: * diff --git a/dom/credentialmanagement/identity/tests/mochitest/head.js b/dom/credentialmanagement/identity/tests/mochitest/head.js new file mode 100644 index 0000000000..393ba9fa23 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/head.js @@ -0,0 +1,24 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var idp_host = "https://example.net"; +var test_path = "/tests/dom/credentialmanagement/identity/tests/mochitest"; +var idp_api = idp_host + test_path; + +async function setupTest(testName) { + ok( + window.location.pathname.includes(testName), + `Must set the right test name when setting up. Test name "${testName}" must be in URL path "${window.location.pathname}"` + ); + let fetchPromise = fetch( + `${idp_api}/server_manifest.sjs?set_test=${testName}` + ); + let focusPromise = SimpleTest.promiseFocus(); + window.open(`${idp_api}/helper_set_cookie.html`, "_blank"); + await focusPromise; + return fetchPromise; +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/helper_set_cookie.html b/dom/credentialmanagement/identity/tests/mochitest/helper_set_cookie.html new file mode 100644 index 0000000000..9f8e410b90 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/helper_set_cookie.html @@ -0,0 +1,8 @@ + + + + +See ya! (This window will close itself) +The cookie was set via HTTP. diff --git a/dom/credentialmanagement/identity/tests/mochitest/helper_set_cookie.html^headers^ b/dom/credentialmanagement/identity/tests/mochitest/helper_set_cookie.html^headers^ new file mode 100644 index 0000000000..c221facafc --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/helper_set_cookie.html^headers^ @@ -0,0 +1 @@ +Set-Cookie: credential=authcookieval; SameSite=None; Secure; Path=/ diff --git a/dom/credentialmanagement/identity/tests/mochitest/mochitest.toml b/dom/credentialmanagement/identity/tests/mochitest/mochitest.toml new file mode 100644 index 0000000000..8522216f1e --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/mochitest.toml @@ -0,0 +1,67 @@ +[DEFAULT] +prefs = [ + "dom.security.credentialmanagement.identity.enabled=true", + "dom.security.credentialmanagement.identity.select_first_in_ui_lists=true", + "dom.security.credentialmanagement.identity.reject_delay.enabled=false", + "privacy.antitracking.enableWebcompat=false", # disables opener heuristic +] +scheme = "https" +skip-if = [ + "xorigin", + "http3", # Bug 1838420 + "http2", +] + +support-files = [ + "head.js", + "helper_set_cookie.html", + "helper_set_cookie.html^headers^", + "/.well-known/web-identity", + "/.well-known/web-identity^headers^", + "server_manifest.sjs", + "server_manifest_wrong_provider_in_manifest.sjs", + "server_metadata.json", + "server_metadata.json^headers^", + "server_simple_accounts.sjs", + "server_simple_idtoken.sjs", + "server_no_accounts_accounts.sjs", + "server_no_accounts_idtoken.sjs", + "server_two_accounts_accounts.sjs", + "server_two_accounts_idtoken.sjs", + "server_two_providers_accounts.sjs", + "server_two_providers_idtoken.sjs", + "server_accounts_error_accounts.sjs", + "server_accounts_error_idtoken.sjs", + "server_idtoken_error_accounts.sjs", + "server_idtoken_error_idtoken.sjs", + "server_accounts_redirect_accounts.sjs", + "server_accounts_redirect_idtoken.sjs", + "server_idtoken_redirect_accounts.sjs", + "server_idtoken_redirect_idtoken.sjs", +] + +["test_accounts_error.html"] + +["test_accounts_redirect.html"] + +["test_delay_reject.html"] + +["test_empty_provider_list.html"] + +["test_get_without_providers.html"] + +["test_idtoken_error.html"] + +["test_idtoken_redirect.html"] + +["test_mediation.html"] + +["test_no_accounts.html"] + +["test_simple.html"] + +["test_two_accounts.html"] + +["test_two_providers.html"] + +["test_wrong_provider_in_manifest.html"] diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_accounts_error_accounts.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_accounts_error_accounts.sjs new file mode 100644 index 0000000000..d0a11ce469 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_accounts_error_accounts.sjs @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setStatusLine(request.httpVersion, 503, "Service Unavailable"); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_accounts_error_idtoken.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_accounts_error_idtoken.sjs new file mode 100644 index 0000000000..a6f6f7c4b1 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_accounts_error_idtoken.sjs @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let responseContent = { + token: "should not be returned", + }; + let body = JSON.stringify(responseContent); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_accounts_redirect_accounts.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_accounts_redirect_accounts.sjs new file mode 100644 index 0000000000..f33da643a0 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_accounts_redirect_accounts.sjs @@ -0,0 +1,10 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Location", "server_simple_accounts.sjs"); + response.setStatusLine(request.httpVersion, 302, "Found"); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_accounts_redirect_idtoken.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_accounts_redirect_idtoken.sjs new file mode 100644 index 0000000000..a6f6f7c4b1 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_accounts_redirect_idtoken.sjs @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let responseContent = { + token: "should not be returned", + }; + let body = JSON.stringify(responseContent); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_error_accounts.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_error_accounts.sjs new file mode 100644 index 0000000000..9baeb51ba5 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_error_accounts.sjs @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let content = { + accounts: [ + { + id: "1234", + given_name: "John", + name: "John Doe", + email: "john_doe@idp.example", + picture: "https://idp.example/profile/123", + approved_clients: ["123", "456", "789"], + }, + ], + }; + let body = JSON.stringify(content); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_error_idtoken.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_error_idtoken.sjs new file mode 100644 index 0000000000..653207672b --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_error_idtoken.sjs @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setStatusLine(request.httpVersion, 503, "Service Unavailable"); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_redirect_accounts.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_redirect_accounts.sjs new file mode 100644 index 0000000000..9baeb51ba5 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_redirect_accounts.sjs @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let content = { + accounts: [ + { + id: "1234", + given_name: "John", + name: "John Doe", + email: "john_doe@idp.example", + picture: "https://idp.example/profile/123", + approved_clients: ["123", "456", "789"], + }, + ], + }; + let body = JSON.stringify(content); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_redirect_idtoken.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_redirect_idtoken.sjs new file mode 100644 index 0000000000..66456eb687 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_redirect_idtoken.sjs @@ -0,0 +1,10 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Location", "server_simple_idtoken.sjs"); + response.setStatusLine(request.httpVersion, 302, "Found"); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs new file mode 100644 index 0000000000..338631632a --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + let params = new URLSearchParams(request.queryString); + let test = params.get("set_test"); + if (test === null) { + test = getState("test"); + } else { + setState("test", test); + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setStatusLine(request.httpVersion, 200, "OK"); + return; + } + + if (request.hasHeader("Cookie")) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Origin") && request.getHeader("Origin") != "null") { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Referer")) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Content-Type", "application/json"); + let content = { + accounts_endpoint: + "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_TESTNAME_accounts.sjs", + client_metadata_endpoint: + "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_metadata.json", + id_assertion_endpoint: + "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_TESTNAME_idtoken.sjs", + }; + let bodyFormat = JSON.stringify(content); + let body = bodyFormat.replaceAll("TESTNAME", test); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_manifest_wrong_provider_in_manifest.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_manifest_wrong_provider_in_manifest.sjs new file mode 100644 index 0000000000..94c60fb731 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_manifest_wrong_provider_in_manifest.sjs @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Content-Type", "application/json"); + let content = { + accounts_endpoint: + "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_simple_accounts.sjs", + client_metadata_endpoint: + "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_simple_metadata.sjs", + id_assertion_endpoint: + "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_simple_idtoken.sjs", + }; + let body = JSON.stringify(content); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_metadata.json b/dom/credentialmanagement/identity/tests/mochitest/server_metadata.json new file mode 100644 index 0000000000..1e16c942b5 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_metadata.json @@ -0,0 +1,4 @@ +{ + "privacy_policy_url": "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/null.txt", + "terms_of_service_url": "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/null.txt" +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_metadata.json^headers^ b/dom/credentialmanagement/identity/tests/mochitest/server_metadata.json^headers^ new file mode 100644 index 0000000000..75875a7cf3 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_metadata.json^headers^ @@ -0,0 +1,2 @@ +Content-Type: application/json +Access-Control-Allow-Origin: * diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_no_accounts_accounts.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_no_accounts_accounts.sjs new file mode 100644 index 0000000000..dac20e4466 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_no_accounts_accounts.sjs @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + if ( + !request.hasHeader("Cookie") || + request.getHeader("Cookie") != "credential=authcookieval" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Sec-Fetch-Dest") || + request.getHeader("Sec-Fetch-Dest") != "webidentity" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Origin") && request.getHeader("Origin") != "null") { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Referer")) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let content = { + accounts: [], + }; + let body = JSON.stringify(content); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_no_accounts_idtoken.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_no_accounts_idtoken.sjs new file mode 100644 index 0000000000..a3ca4ce31d --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_no_accounts_idtoken.sjs @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function readStream(inputStream) { + let available = 0; + let result = []; + while ((available = inputStream.available()) > 0) { + result.push(inputStream.readBytes(available)); + } + return result.join(""); +} + +function handleRequest(request, response) { + if (request.method != "POST") { + response.setStatusLine(request.httpVersion, 405, "Method Not Allowed"); + return; + } + if ( + !request.hasHeader("Cookie") || + request.getHeader("Cookie") != "credential=authcookieval" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Sec-Fetch-Dest") || + request.getHeader("Sec-Fetch-Dest") != "webidentity" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Referer") || + request.getHeader("Referer") != "https://example.com/" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Origin") || + request.getHeader("Origin") != "https://example.com" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let requestContent = readStream( + new BinaryInputStream(request.bodyInputStream) + ); + let responseContent = { + token: requestContent, + }; + let body = JSON.stringify(responseContent); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_simple_accounts.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_simple_accounts.sjs new file mode 100644 index 0000000000..6ebce36802 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_simple_accounts.sjs @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + if ( + !request.hasHeader("Cookie") || + request.getHeader("Cookie") != "credential=authcookieval" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Sec-Fetch-Dest") || + request.getHeader("Sec-Fetch-Dest") != "webidentity" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Origin") && request.getHeader("Origin") != "null") { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Referer")) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let content = { + accounts: [ + { + id: "1234", + given_name: "John", + name: "John Doe", + email: "john_doe@idp.example", + picture: "https://idp.example/profile/123", + approved_clients: ["123", "456", "789"], + }, + ], + }; + let body = JSON.stringify(content); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_simple_idtoken.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_simple_idtoken.sjs new file mode 100644 index 0000000000..a3ca4ce31d --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_simple_idtoken.sjs @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function readStream(inputStream) { + let available = 0; + let result = []; + while ((available = inputStream.available()) > 0) { + result.push(inputStream.readBytes(available)); + } + return result.join(""); +} + +function handleRequest(request, response) { + if (request.method != "POST") { + response.setStatusLine(request.httpVersion, 405, "Method Not Allowed"); + return; + } + if ( + !request.hasHeader("Cookie") || + request.getHeader("Cookie") != "credential=authcookieval" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Sec-Fetch-Dest") || + request.getHeader("Sec-Fetch-Dest") != "webidentity" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Referer") || + request.getHeader("Referer") != "https://example.com/" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Origin") || + request.getHeader("Origin") != "https://example.com" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let requestContent = readStream( + new BinaryInputStream(request.bodyInputStream) + ); + let responseContent = { + token: requestContent, + }; + let body = JSON.stringify(responseContent); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_two_accounts_accounts.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_two_accounts_accounts.sjs new file mode 100644 index 0000000000..f9d60183a1 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_two_accounts_accounts.sjs @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + if ( + !request.hasHeader("Cookie") || + request.getHeader("Cookie") != "credential=authcookieval" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Sec-Fetch-Dest") || + request.getHeader("Sec-Fetch-Dest") != "webidentity" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Origin") && request.getHeader("Origin") != "null") { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Referer")) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let content = { + accounts: [ + { + id: "1234", + given_name: "John", + name: "John Doe", + email: "john_doe@idp.example", + picture: "https://idp.example/profile/123", + approved_clients: ["123", "456", "789"], + }, + { + id: "5678", + given_name: "Johnny", + name: "Johnny", + email: "johnny@idp.example", + picture: "https://idp.example/profile/456", + approved_clients: ["abc", "def", "ghi"], + }, + ], + }; + let body = JSON.stringify(content); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_two_accounts_idtoken.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_two_accounts_idtoken.sjs new file mode 100644 index 0000000000..a3ca4ce31d --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_two_accounts_idtoken.sjs @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function readStream(inputStream) { + let available = 0; + let result = []; + while ((available = inputStream.available()) > 0) { + result.push(inputStream.readBytes(available)); + } + return result.join(""); +} + +function handleRequest(request, response) { + if (request.method != "POST") { + response.setStatusLine(request.httpVersion, 405, "Method Not Allowed"); + return; + } + if ( + !request.hasHeader("Cookie") || + request.getHeader("Cookie") != "credential=authcookieval" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Sec-Fetch-Dest") || + request.getHeader("Sec-Fetch-Dest") != "webidentity" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Referer") || + request.getHeader("Referer") != "https://example.com/" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Origin") || + request.getHeader("Origin") != "https://example.com" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let requestContent = readStream( + new BinaryInputStream(request.bodyInputStream) + ); + let responseContent = { + token: requestContent, + }; + let body = JSON.stringify(responseContent); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_two_providers_accounts.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_two_providers_accounts.sjs new file mode 100644 index 0000000000..25060b850e --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_two_providers_accounts.sjs @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + if ( + !request.hasHeader("Cookie") || + request.getHeader("Cookie") != "credential=authcookieval" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Origin") && request.getHeader("Origin") != "null") { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Referer")) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let content = { + accounts: [ + { + id: "1234", + given_name: "John", + name: "John Doe", + email: "john_doe@idp.example", + picture: "https://idp.example/profile/123", + approved_clients: ["123", "456", "789"], + }, + { + id: "5678", + given_name: "Johnny", + name: "Johnny", + email: "johnny@idp.example", + picture: "https://idp.example/profile/456", + approved_clients: ["abc", "def", "ghi"], + }, + ], + }; + let body = JSON.stringify(content); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_two_providers_idtoken.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_two_providers_idtoken.sjs new file mode 100644 index 0000000000..01b61ff33d --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_two_providers_idtoken.sjs @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function readStream(inputStream) { + let available = 0; + let result = []; + while ((available = inputStream.available()) > 0) { + result.push(inputStream.readBytes(available)); + } + return result.join(""); +} + +function handleRequest(request, response) { + if (request.method != "POST") { + response.setStatusLine(request.httpVersion, 405, "Method Not Allowed"); + return; + } + if ( + !request.hasHeader("Cookie") || + request.getHeader("Cookie") != "credential=authcookieval" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Referer") || + request.getHeader("Referer") != "https://example.com/" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Origin") || + request.getHeader("Origin") != "https://example.com" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let requestContent = readStream( + new BinaryInputStream(request.bodyInputStream) + ); + let responseContent = { + token: requestContent, + }; + let body = JSON.stringify(responseContent); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_accounts_error.html b/dom/credentialmanagement/identity/tests/mochitest/test_accounts_error.html new file mode 100644 index 0000000000..ca0f85b110 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/test_accounts_error.html @@ -0,0 +1,37 @@ + + + + + Server Error On Accounts Endpoint + + + + + + +

+ +

+
+
diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_accounts_redirect.html b/dom/credentialmanagement/identity/tests/mochitest/test_accounts_redirect.html
new file mode 100644
index 0000000000..99b897d35e
--- /dev/null
+++ b/dom/credentialmanagement/identity/tests/mochitest/test_accounts_redirect.html
@@ -0,0 +1,37 @@
+
+
+
+  
+  Server Redirect On Accounts Endpoint
+  
+  
+  
+  
+
+
+

+ +

+
+
diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_delay_reject.html b/dom/credentialmanagement/identity/tests/mochitest/test_delay_reject.html
new file mode 100644
index 0000000000..0151f4b6c4
--- /dev/null
+++ b/dom/credentialmanagement/identity/tests/mochitest/test_delay_reject.html
@@ -0,0 +1,39 @@
+
+
+
+  
+  Delay Reject
+  
+  
+  
+  
+
+
+

+ +

+
+
diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_empty_provider_list.html b/dom/credentialmanagement/identity/tests/mochitest/test_empty_provider_list.html
new file mode 100644
index 0000000000..ad5b6ea28c
--- /dev/null
+++ b/dom/credentialmanagement/identity/tests/mochitest/test_empty_provider_list.html
@@ -0,0 +1,34 @@
+
+
+
+  
+  Empty Provider List
+  
+  
+  
+  
+
+
+

+ +

+
+
diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_get_without_providers.html b/dom/credentialmanagement/identity/tests/mochitest/test_get_without_providers.html
new file mode 100644
index 0000000000..4425abf5aa
--- /dev/null
+++ b/dom/credentialmanagement/identity/tests/mochitest/test_get_without_providers.html
@@ -0,0 +1,32 @@
+
+
+
+  
+  No Providers Specified
+  
+  
+  
+  
+
+
+

+ +

+
+
diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_idtoken_error.html b/dom/credentialmanagement/identity/tests/mochitest/test_idtoken_error.html
new file mode 100644
index 0000000000..ddc6716081
--- /dev/null
+++ b/dom/credentialmanagement/identity/tests/mochitest/test_idtoken_error.html
@@ -0,0 +1,37 @@
+
+
+
+  
+  Server Error On Token Endpoint
+  
+  
+  
+  
+
+
+

+ +

+
+
diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_idtoken_redirect.html b/dom/credentialmanagement/identity/tests/mochitest/test_idtoken_redirect.html
new file mode 100644
index 0000000000..88512a1d22
--- /dev/null
+++ b/dom/credentialmanagement/identity/tests/mochitest/test_idtoken_redirect.html
@@ -0,0 +1,37 @@
+
+
+
+  
+  Server Redirect On Token Endpoint
+  
+  
+  
+  
+
+
+

+ +

+
+
diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_mediation.html b/dom/credentialmanagement/identity/tests/mochitest/test_mediation.html
new file mode 100644
index 0000000000..a36c20a504
--- /dev/null
+++ b/dom/credentialmanagement/identity/tests/mochitest/test_mediation.html
@@ -0,0 +1,38 @@
+
+
+
+  
+  Mediation Test
+  
+  
+  
+  
+
+
+

+ +

+
+
diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_no_accounts.html b/dom/credentialmanagement/identity/tests/mochitest/test_no_accounts.html
new file mode 100644
index 0000000000..90c3335eda
--- /dev/null
+++ b/dom/credentialmanagement/identity/tests/mochitest/test_no_accounts.html
@@ -0,0 +1,37 @@
+
+
+
+  
+  No Accounts in the List
+  
+  
+  
+  
+
+
+

+ +

+
+
diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_simple.html b/dom/credentialmanagement/identity/tests/mochitest/test_simple.html
new file mode 100644
index 0000000000..39d34f3d5f
--- /dev/null
+++ b/dom/credentialmanagement/identity/tests/mochitest/test_simple.html
@@ -0,0 +1,46 @@
+
+
+
+  
+  Happypath Test
+  
+  
+  
+  
+
+
+

+ +

+
+
diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_two_accounts.html b/dom/credentialmanagement/identity/tests/mochitest/test_two_accounts.html
new file mode 100644
index 0000000000..36e99adf75
--- /dev/null
+++ b/dom/credentialmanagement/identity/tests/mochitest/test_two_accounts.html
@@ -0,0 +1,46 @@
+
+
+
+  
+  Two Accounts in the List
+  
+  
+  
+  
+
+
+

+ +

+
+
diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_two_providers.html b/dom/credentialmanagement/identity/tests/mochitest/test_two_providers.html
new file mode 100644
index 0000000000..5533f71064
--- /dev/null
+++ b/dom/credentialmanagement/identity/tests/mochitest/test_two_providers.html
@@ -0,0 +1,52 @@
+
+
+
+  
+  Two Providers in a Credential.get()
+  
+  
+  
+  
+
+
+

+ +

+
+
diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_wrong_provider_in_manifest.html b/dom/credentialmanagement/identity/tests/mochitest/test_wrong_provider_in_manifest.html
new file mode 100644
index 0000000000..8ff1afe04d
--- /dev/null
+++ b/dom/credentialmanagement/identity/tests/mochitest/test_wrong_provider_in_manifest.html
@@ -0,0 +1,37 @@
+
+
+
+  
+  Manifest Disagreement
+  
+  
+  
+  
+
+
+

+ +

+
+
diff --git a/dom/credentialmanagement/identity/tests/mochitest/web-identity b/dom/credentialmanagement/identity/tests/mochitest/web-identity
new file mode 100644
index 0000000000..33dc9c455b
--- /dev/null
+++ b/dom/credentialmanagement/identity/tests/mochitest/web-identity
@@ -0,0 +1 @@
+{"provider_urls": ["https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs"]}
diff --git a/dom/credentialmanagement/identity/tests/mochitest/web-identity^headers^ b/dom/credentialmanagement/identity/tests/mochitest/web-identity^headers^
new file mode 100644
index 0000000000..75875a7cf3
--- /dev/null
+++ b/dom/credentialmanagement/identity/tests/mochitest/web-identity^headers^
@@ -0,0 +1,2 @@
+Content-Type: application/json
+Access-Control-Allow-Origin: *
diff --git a/dom/credentialmanagement/moz.build b/dom/credentialmanagement/moz.build
new file mode 100644
index 0000000000..17f02573d4
--- /dev/null
+++ b/dom/credentialmanagement/moz.build
@@ -0,0 +1,28 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+    BUG_COMPONENT = ("Core", "DOM: Web Authentication")
+
+DIRS += ["identity"]
+
+EXPORTS.mozilla.dom += [
+    "Credential.h",
+    "CredentialsContainer.h",
+]
+
+UNIFIED_SOURCES += [
+    "Credential.cpp",
+    "CredentialsContainer.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+MOCHITEST_MANIFESTS += ["tests/mochitest/mochitest.toml"]
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]
+CRASHTEST_MANIFESTS += ["tests/crashtests/crashtests.list"]
diff --git a/dom/credentialmanagement/tests/browser/browser.toml b/dom/credentialmanagement/tests/browser/browser.toml
new file mode 100644
index 0000000000..be685270ac
--- /dev/null
+++ b/dom/credentialmanagement/tests/browser/browser.toml
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+["browser_active_document.js"]
diff --git a/dom/credentialmanagement/tests/browser/browser_active_document.js b/dom/credentialmanagement/tests/browser/browser_active_document.js
new file mode 100644
index 0000000000..eced461630
--- /dev/null
+++ b/dom/credentialmanagement/tests/browser/browser_active_document.js
@@ -0,0 +1,139 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const TEST_URL = "https://example.com/";
+
+function arrivingHereIsBad(aResult) {
+  ok(false, "Bad result! Received a: " + aResult);
+}
+
+function expectNotAllowedError(aResult) {
+  let expected = "NotAllowedError";
+  is(aResult.slice(0, expected.length), expected, `Expecting a ${expected}`);
+}
+
+function promiseMakeCredential(tab) {
+  return ContentTask.spawn(tab.linkedBrowser, null, async function () {
+    const cose_alg_ECDSA_w_SHA256 = -7;
+
+    let publicKey = {
+      rp: { id: content.document.domain, name: "none", icon: "none" },
+      user: {
+        id: new Uint8Array(),
+        name: "none",
+        icon: "none",
+        displayName: "none",
+      },
+      challenge: content.crypto.getRandomValues(new Uint8Array(16)),
+      timeout: 5000, // the minimum timeout is actually 15 seconds
+      pubKeyCredParams: [{ type: "public-key", alg: cose_alg_ECDSA_w_SHA256 }],
+    };
+
+    return content.navigator.credentials.create({ publicKey });
+  });
+}
+
+function promiseGetAssertion(tab) {
+  return ContentTask.spawn(tab.linkedBrowser, null, async function () {
+    let newCredential = {
+      type: "public-key",
+      id: content.crypto.getRandomValues(new Uint8Array(16)),
+      transports: ["usb"],
+    };
+
+    let publicKey = {
+      challenge: content.crypto.getRandomValues(new Uint8Array(16)),
+      timeout: 5000, // the minimum timeout is actually 15 seconds
+      rpId: content.document.domain,
+      allowCredentials: [newCredential],
+    };
+
+    return content.navigator.credentials.get({ publicKey });
+  });
+}
+
+add_task(async function test_setup() {
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      ["security.webauth.webauthn", true],
+      ["security.webauth.webauthn_enable_softtoken", true],
+      ["security.webauth.webauthn_enable_usbtoken", false],
+    ],
+  });
+});
+
+add_task(async function test_background_tab() {
+  // Open two tabs, the last one will selected.
+  let tab_bg = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+  let tab_fg = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+  // Requests from background tabs must fail.
+  await promiseMakeCredential(tab_bg)
+    .then(arrivingHereIsBad)
+    .catch(expectNotAllowedError);
+
+  // Requests from background tabs must fail.
+  await promiseGetAssertion(tab_bg)
+    .then(arrivingHereIsBad)
+    .catch(expectNotAllowedError);
+
+  // Close tabs.
+  await BrowserTestUtils.removeTab(tab_bg);
+  await BrowserTestUtils.removeTab(tab_fg);
+});
+
+add_task(async function test_background_window() {
+  // Open a tab, then a new window.
+  let tab_bg = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+  let win = await BrowserTestUtils.openNewBrowserWindow();
+
+  // Wait until the new window is really focused.
+  await new Promise(resolve => SimpleTest.waitForFocus(resolve, win));
+
+  // Requests from selected tabs not in the active window must fail.
+  await promiseMakeCredential(tab_bg)
+    .then(arrivingHereIsBad)
+    .catch(expectNotAllowedError);
+
+  // Requests from selected tabs not in the active window must fail.
+  await promiseGetAssertion(tab_bg)
+    .then(arrivingHereIsBad)
+    .catch(expectNotAllowedError);
+
+  // Close tab and window.
+  await BrowserTestUtils.closeWindow(win);
+  await BrowserTestUtils.removeTab(tab_bg);
+});
+
+add_task(async function test_minimized() {
+  // Minimizing windows doesn't supported in headless mode.
+  if (Services.env.get("MOZ_HEADLESS")) {
+    return;
+  }
+
+  // Open a window with a tab.
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+  // Minimize the window.
+  window.minimize();
+  await TestUtils.waitForCondition(() => !tab.linkedBrowser.docShellIsActive);
+
+  // Requests from minimized windows must fail.
+  await promiseMakeCredential(tab)
+    .then(arrivingHereIsBad)
+    .catch(expectNotAllowedError);
+
+  // Requests from minimized windows must fail.
+  await promiseGetAssertion(tab)
+    .then(arrivingHereIsBad)
+    .catch(expectNotAllowedError);
+
+  // Restore the window.
+  await new Promise(resolve => SimpleTest.waitForFocus(resolve, window));
+
+  // Close tab.
+  await BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/credentialmanagement/tests/crashtests/bug1691963.html b/dom/credentialmanagement/tests/crashtests/bug1691963.html
new file mode 100644
index 0000000000..f7ef34622f
--- /dev/null
+++ b/dom/credentialmanagement/tests/crashtests/bug1691963.html
@@ -0,0 +1,28 @@
+
+
+
+  
+
+
diff --git a/dom/credentialmanagement/tests/crashtests/crashtests.list b/dom/credentialmanagement/tests/crashtests/crashtests.list
new file mode 100644
index 0000000000..dcd014d6ec
--- /dev/null
+++ b/dom/credentialmanagement/tests/crashtests/crashtests.list
@@ -0,0 +1 @@
+load bug1691963.html
diff --git a/dom/credentialmanagement/tests/mochitest/frame_credman_iframes.html b/dom/credentialmanagement/tests/mochitest/frame_credman_iframes.html
new file mode 100644
index 0000000000..e7dbd40b34
--- /dev/null
+++ b/dom/credentialmanagement/tests/mochitest/frame_credman_iframes.html
@@ -0,0 +1,105 @@
+
+
+
+  Embedded Frame for Credential Management: Prohibit use in cross-origin iframes
+  
+  
+
+
+
+
+
+
+ + + diff --git a/dom/credentialmanagement/tests/mochitest/mochitest.toml b/dom/credentialmanagement/tests/mochitest/mochitest.toml new file mode 100644 index 0000000000..d8d142d9d8 --- /dev/null +++ b/dom/credentialmanagement/tests/mochitest/mochitest.toml @@ -0,0 +1,14 @@ +[DEFAULT] +support-files = ["frame_credman_iframes.html"] +scheme = "https" + +["test_credman_empty_option.html"] + +["test_credman_iframes.html"] +skip-if = [ + "xorigin", # Application time out + "win10_2009", # Bug 1718296 + "win11_2009", # Bug 1718296 + "http3", + "http2", +] diff --git a/dom/credentialmanagement/tests/mochitest/test_credman_empty_option.html b/dom/credentialmanagement/tests/mochitest/test_credman_empty_option.html new file mode 100644 index 0000000000..4e582a9f8e --- /dev/null +++ b/dom/credentialmanagement/tests/mochitest/test_credman_empty_option.html @@ -0,0 +1,40 @@ + + + Credential Management: Handle requests with empty options + + + + + +

Credential Management: Handle requests with empty options

+ + + + diff --git a/dom/credentialmanagement/tests/mochitest/test_credman_iframes.html b/dom/credentialmanagement/tests/mochitest/test_credman_iframes.html new file mode 100644 index 0000000000..b77a868392 --- /dev/null +++ b/dom/credentialmanagement/tests/mochitest/test_credman_iframes.html @@ -0,0 +1,88 @@ + + + Credential Management: Prohibit use in cross-origin iframes + + + + + +

Credential Management: Prohibit use in cross-origin iframes

+ + +
+

Same Origin Test

+ + +

Cross-Origin Test

+ +
+ + + + -- cgit v1.2.3