summaryrefslogtreecommitdiffstats
path: root/dom/credentialmanagement
diff options
context:
space:
mode:
Diffstat (limited to 'dom/credentialmanagement')
-rw-r--r--dom/credentialmanagement/Credential.cpp40
-rw-r--r--dom/credentialmanagement/Credential.h50
-rw-r--r--dom/credentialmanagement/CredentialsContainer.cpp310
-rw-r--r--dom/credentialmanagement/CredentialsContainer.h53
-rw-r--r--dom/credentialmanagement/identity/IPCIdentityCredential.ipdlh18
-rw-r--r--dom/credentialmanagement/identity/IdentityCredential.cpp1114
-rw-r--r--dom/credentialmanagement/identity/IdentityCredential.h314
-rw-r--r--dom/credentialmanagement/identity/IdentityCredentialSerializationHelpers.h48
-rw-r--r--dom/credentialmanagement/identity/IdentityNetworkHelpers.h113
-rw-r--r--dom/credentialmanagement/identity/moz.build28
-rw-r--r--dom/credentialmanagement/identity/tests/browser/browser.toml21
-rw-r--r--dom/credentialmanagement/identity/tests/browser/browser_close_prompt_on_timeout.js63
-rw-r--r--dom/credentialmanagement/identity/tests/browser/browser_single_concurrent_identity_request.js51
-rw-r--r--dom/credentialmanagement/identity/tests/browser/server_accounts.json12
-rw-r--r--dom/credentialmanagement/identity/tests/browser/server_accounts.json^headers^3
-rw-r--r--dom/credentialmanagement/identity/tests/browser/server_idtoken.json1
-rw-r--r--dom/credentialmanagement/identity/tests/browser/server_idtoken.json^headers^3
-rw-r--r--dom/credentialmanagement/identity/tests/browser/server_manifest.json5
-rw-r--r--dom/credentialmanagement/identity/tests/browser/server_manifest.json^headers^2
-rw-r--r--dom/credentialmanagement/identity/tests/browser/server_metadata.json4
-rw-r--r--dom/credentialmanagement/identity/tests/browser/server_metadata.json^headers^2
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/head.js24
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/helper_set_cookie.html8
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/helper_set_cookie.html^headers^1
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/mochitest.toml67
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_accounts_error_accounts.sjs9
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_accounts_error_idtoken.sjs15
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_accounts_redirect_accounts.sjs10
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_accounts_redirect_idtoken.sjs15
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_idtoken_error_accounts.sjs24
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_idtoken_error_idtoken.sjs9
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_idtoken_redirect_accounts.sjs24
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_idtoken_redirect_idtoken.sjs10
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs44
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_manifest_wrong_provider_in_manifest.sjs19
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_metadata.json4
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_metadata.json^headers^2
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_no_accounts_accounts.sjs38
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_no_accounts_idtoken.sjs66
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_simple_accounts.sjs47
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_simple_idtoken.sjs66
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_two_accounts_accounts.sjs55
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_two_accounts_idtoken.sjs66
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_two_providers_accounts.sjs48
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/server_two_providers_idtoken.sjs59
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/test_accounts_error.html37
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/test_accounts_redirect.html37
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/test_delay_reject.html39
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/test_empty_provider_list.html34
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/test_get_without_providers.html32
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/test_idtoken_error.html37
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/test_idtoken_redirect.html37
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/test_mediation.html38
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/test_no_accounts.html37
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/test_simple.html46
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/test_two_accounts.html46
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/test_two_providers.html52
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/test_wrong_provider_in_manifest.html37
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/web-identity1
-rw-r--r--dom/credentialmanagement/identity/tests/mochitest/web-identity^headers^2
-rw-r--r--dom/credentialmanagement/moz.build28
-rw-r--r--dom/credentialmanagement/tests/browser/browser.toml3
-rw-r--r--dom/credentialmanagement/tests/browser/browser_active_document.js139
-rw-r--r--dom/credentialmanagement/tests/crashtests/bug1691963.html28
-rw-r--r--dom/credentialmanagement/tests/crashtests/crashtests.list1
-rw-r--r--dom/credentialmanagement/tests/mochitest/frame_credman_iframes.html105
-rw-r--r--dom/credentialmanagement/tests/mochitest/mochitest.toml14
-rw-r--r--dom/credentialmanagement/tests/mochitest/test_credman_empty_option.html40
-rw-r--r--dom/credentialmanagement/tests/mochitest/test_credman_iframes.html88
69 files changed, 3943 insertions, 0 deletions
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<JSObject*> 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<JSObject*> aGivenProto) override;
+
+ void GetId(nsAString& aId) const;
+
+ void GetType(nsAString& aType) const;
+
+ void SetId(const nsAString& aId);
+
+ void SetType(const nsAString& aType);
+
+ private:
+ nsCOMPtr<nsPIDOMWindowInner> 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<Promise> CreatePromise(nsPIDOMWindowInner* aParent,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(aParent);
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aParent);
+ if (NS_WARN_IF(!global)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+ RefPtr<Promise> promise = Promise::Create(global, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ return promise.forget();
+}
+
+already_AddRefed<Promise> CreateAndRejectWithNotAllowed(
+ nsPIDOMWindowInner* aParent, ErrorResult& aRv) {
+ MOZ_ASSERT(aParent);
+ RefPtr<Promise> promise = CreatePromise(aParent, aRv);
+ if (!promise) {
+ return nullptr;
+ }
+ promise->MaybeRejectWithNotAllowedError(
+ "CredentialContainer request is not allowed."_ns);
+ return promise.forget();
+}
+
+already_AddRefed<Promise> CreateAndRejectWithNotSupported(
+ nsPIDOMWindowInner* aParent, ErrorResult& aRv) {
+ MOZ_ASSERT(aParent);
+ RefPtr<Promise> 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<Document> 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<Document> 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<WebAuthnManager> CredentialsContainer::GetWebAuthnManager() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ EnsureWebAuthnManager();
+ RefPtr<WebAuthnManager> ref = mManager;
+ return ref.forget();
+}
+
+JSObject* CredentialsContainer::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return CredentialsContainer_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+already_AddRefed<Promise> 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> promise = CreatePromise(mParent, aRv);
+ if (!promise) {
+ return nullptr;
+ }
+ promise->MaybeRejectWithTypeError<MSG_INVALID_ENUM_VALUE>(
+ "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> promise = CreatePromise(mParent, aRv);
+ if (!promise) {
+ return nullptr;
+ }
+
+ if (conditionallyMediated) {
+ promise->MaybeRejectWithTypeError<MSG_INVALID_ENUM_VALUE>(
+ "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<CredentialsContainer> self = this;
+
+ IdentityCredential::DiscoverFromExternalSource(
+ mParent, aOptions, IsSameOriginWithAncestors(mParent))
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self, promise](const RefPtr<IdentityCredential>& 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<Promise> 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<Promise> 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<Promise> CredentialsContainer::PreventSilentAccess(
+ ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
+ if (NS_WARN_IF(!global)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<Promise> 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<WebAuthnManager> GetWebAuthnManager();
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ already_AddRefed<Promise> Get(const CredentialRequestOptions& aOptions,
+ ErrorResult& aRv);
+
+ already_AddRefed<Promise> Create(const CredentialCreationOptions& aOptions,
+ ErrorResult& aRv);
+
+ already_AddRefed<Promise> Store(const Credential& aCredential,
+ ErrorResult& aRv);
+
+ already_AddRefed<Promise> PreventSilentAccess(ErrorResult& aRv);
+
+ private:
+ ~CredentialsContainer();
+
+ void EnsureWebAuthnManager();
+
+ nsCOMPtr<nsPIDOMWindowInner> mParent;
+ RefPtr<WebAuthnManager> 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<JSObject*> 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::GetIdentityCredentialPromise>
+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<WindowGlobalChild> wgc = aParent->GetWindowGlobalChild();
+ MOZ_ASSERT(wgc);
+ RefPtr<IdentityCredential> 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::GetIPCIdentityCredentialPromise>
+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<IdentityCredential::GetIPCIdentityCredentialPromise::Private> result =
+ new IdentityCredential::GetIPCIdentityCredentialPromise::Private(
+ __func__);
+
+ nsCOMPtr<nsIPrincipal> principal(aPrincipal);
+ RefPtr<CanonicalBrowsingContext> browsingContext(aBrowsingContext);
+
+ RefPtr<nsITimer> 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<RefPtr<GetManifestPromise>> manifestPromises;
+ for (const IdentityProviderConfig& provider : aOptions.mProviders.Value()) {
+ RefPtr<GetManifestPromise> 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<MozPromise>
+ CopyableTArray<MozPromise<IdentityProviderAPIConfig, nsresult,
+ true>::ResolveOrRejectValue>
+ results = aResults;
+ const Sequence<MozPromise<IdentityProviderAPIConfig, nsresult,
+ true>::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::GetIPCIdentityCredentialPromise>
+IdentityCredential::CreateCredential(
+ nsIPrincipal* aPrincipal, BrowsingContext* aBrowsingContext,
+ const IdentityProviderConfig& aProvider,
+ const IdentityProviderAPIConfig& aManifest) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aBrowsingContext);
+
+ nsCOMPtr<nsIPrincipal> argumentPrincipal = aPrincipal;
+ RefPtr<BrowsingContext> browsingContext(aBrowsingContext);
+
+ return IdentityCredential::FetchAccountList(argumentPrincipal, aProvider,
+ aManifest)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [argumentPrincipal, browsingContext, aProvider](
+ const std::tuple<IdentityProviderAPIConfig,
+ IdentityProviderAccountList>& 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<IdentityProviderAPIConfig,
+ IdentityProviderAccount>& 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<IdentityProviderAPIConfig,
+ IdentityProviderAccount>& 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<IdentityProviderToken, IdentityProviderAccount>&
+ 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::ValidationPromise>
+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<nsIURI> configURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(configURI), configLocation);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return IdentityCredential::ValidationPromise::CreateAndReject(rv, __func__);
+ }
+ RefPtr<nsEffectiveTLDService> 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 =
+ NullPrincipal::CreateWithInheritedAttributes(aPrincipal);
+ nsIXPConnect* xpc = nsContentUtils::XPConnect();
+ MOZ_ASSERT(xpc, "This should never be null!");
+ nsCOMPtr<nsIGlobalObject> global;
+ AutoJSAPI jsapi;
+ jsapi.Init();
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JSObject*> 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<InternalRequest>(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> request =
+ new Request(global, std::move(internalRequest), nullptr);
+
+ return FetchJSONStructure<IdentityProviderWellKnown>(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::GetManifestPromise>
+IdentityCredential::FetchInternalManifest(
+ nsIPrincipal* aPrincipal, const IdentityProviderConfig& aProvider) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ // Build the URL
+ nsCString configLocation = aProvider.mConfigURL;
+
+ // Create the global
+ RefPtr<NullPrincipal> nullPrincipal =
+ NullPrincipal::CreateWithInheritedAttributes(aPrincipal);
+ nsIXPConnect* xpc = nsContentUtils::XPConnect();
+ MOZ_ASSERT(xpc, "This should never be null!");
+ nsCOMPtr<nsIGlobalObject> global;
+ AutoJSAPI jsapi;
+ jsapi.Init();
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JSObject*> 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<InternalRequest>(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> request =
+ new Request(global, std::move(internalRequest), nullptr);
+ return FetchJSONStructure<IdentityProviderAPIConfig>(request);
+}
+
+// static
+RefPtr<IdentityCredential::GetAccountListPromise>
+IdentityCredential::FetchAccountList(
+ nsIPrincipal* aPrincipal, const IdentityProviderConfig& aProvider,
+ const IdentityProviderAPIConfig& aManifest) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ // Build the URL
+ nsCOMPtr<nsIURI> baseURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(baseURI), aProvider.mConfigURL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return IdentityCredential::GetAccountListPromise::CreateAndReject(rv,
+ __func__);
+ }
+ nsCOMPtr<nsIURI> 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<nsIPrincipal> idpPrincipal = BasePrincipal::CreateContentPrincipal(
+ idpURI, aPrincipal->OriginAttributesRef());
+ nsCOMPtr<nsIPrincipal> nullPrincipal =
+ NullPrincipal::CreateWithInheritedAttributes(aPrincipal);
+ AutoTArray<nsCOMPtr<nsIPrincipal>, 2> allowList = {idpPrincipal,
+ nullPrincipal};
+ RefPtr<ExpandedPrincipal> expandedPrincipal =
+ ExpandedPrincipal::Create(allowList, aPrincipal->OriginAttributesRef());
+
+ // Create the global
+ nsIXPConnect* xpc = nsContentUtils::XPConnect();
+ MOZ_ASSERT(xpc, "This should never be null!");
+ nsCOMPtr<nsIGlobalObject> global;
+ AutoJSAPI jsapi;
+ jsapi.Init();
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JSObject*> 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<InternalRequest>(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> request =
+ new Request(global, std::move(internalRequest), nullptr);
+
+ return FetchJSONStructure<IdentityProviderAccountList>(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::GetTokenPromise> IdentityCredential::FetchToken(
+ nsIPrincipal* aPrincipal, const IdentityProviderConfig& aProvider,
+ const IdentityProviderAPIConfig& aManifest,
+ const IdentityProviderAccount& aAccount) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ // Build the URL
+ nsCOMPtr<nsIURI> 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<nsIURI> 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<nsIGlobalObject> global;
+ AutoJSAPI jsapi;
+ jsapi.Init();
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JSObject*> 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<InternalRequest>(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<nsIInputStream> streamBody;
+ rv = NS_NewCStringInputStream(getter_AddRefs(streamBody), bodyCString);
+ if (NS_FAILED(rv)) {
+ return IdentityCredential::GetTokenPromise::CreateAndReject(
+ NS_ERROR_FAILURE, __func__);
+ }
+
+ IgnoredErrorResult error;
+ RefPtr<InternalHeaders> 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> request =
+ new Request(global, std::move(internalRequest), nullptr);
+ return FetchJSONStructure<IdentityProviderToken>(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::GetMetadataPromise>
+IdentityCredential::FetchMetadata(nsIPrincipal* aPrincipal,
+ const IdentityProviderConfig& aProvider,
+ const IdentityProviderAPIConfig& aManifest) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(aPrincipal);
+ // Build the URL
+ nsCOMPtr<nsIURI> 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<nsIURI> 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<nsIGlobalObject> global;
+ AutoJSAPI jsapi;
+ jsapi.Init();
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JSObject*> 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<InternalRequest>(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> request =
+ new Request(global, std::move(internalRequest), nullptr);
+ return FetchJSONStructure<IdentityProviderClientMetadata>(request);
+}
+
+// static
+RefPtr<IdentityCredential::GetIdentityProviderConfigWithManifestPromise>
+IdentityCredential::PromptUserToSelectProvider(
+ BrowsingContext* aBrowsingContext,
+ const Sequence<IdentityProviderConfig>& aProviders,
+ const Sequence<GetManifestPromise::ResolveOrRejectValue>& 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<nsIIdentityCredentialPromptService> icPromptService =
+ mozilla::components::IdentityCredentialPromptService::Service(&error);
+ if (NS_WARN_IF(!icPromptService)) {
+ resultPromise->Reject(error, __func__);
+ return resultPromise;
+ }
+
+ nsCOMPtr<nsIXPConnectWrappedJS> wrapped = do_QueryInterface(icPromptService);
+ AutoJSAPI jsapi;
+ if (NS_WARN_IF(!jsapi.Init(wrapped->GetJSObjectGlobal()))) {
+ resultPromise->Reject(NS_ERROR_FAILURE, __func__);
+ return resultPromise;
+ }
+
+ JS::Rooted<JS::Value> 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<ResolveValue>
+ Sequence<Nullable<IdentityProviderAPIConfig>> 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<IdentityProviderAPIConfig>(), fallible))) {
+ resultPromise->Reject(NS_ERROR_FAILURE, __func__);
+ return resultPromise;
+ }
+ }
+ }
+ JS::Rooted<JS::Value> manifestsJS(jsapi.cx());
+ success = ToJSValue(jsapi.cx(), manifests, &manifestsJS);
+ if (NS_WARN_IF(!success)) {
+ resultPromise->Reject(NS_ERROR_FAILURE, __func__);
+ return resultPromise;
+ }
+
+ RefPtr<Promise> showPromptPromise;
+ icPromptService->ShowProviderPrompt(aBrowsingContext, providersJS,
+ manifestsJS,
+ getter_AddRefs(showPromptPromise));
+
+ showPromptPromise->AddCallbacksWithCycleCollectedArgs(
+ [aProviders, aManifests, resultPromise](
+ JSContext*, JS::Handle<JS::Value> 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<JS::Value> 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::GetAccountPromise>
+IdentityCredential::PromptUserToSelectAccount(
+ BrowsingContext* aBrowsingContext,
+ const IdentityProviderAccountList& aAccounts,
+ const IdentityProviderConfig& aProvider,
+ const IdentityProviderAPIConfig& aManifest) {
+ MOZ_ASSERT(aBrowsingContext);
+ RefPtr<IdentityCredential::GetAccountPromise::Private> resultPromise =
+ new IdentityCredential::GetAccountPromise::Private(__func__);
+
+ if (NS_WARN_IF(!aBrowsingContext)) {
+ resultPromise->Reject(NS_ERROR_FAILURE, __func__);
+ return resultPromise;
+ }
+
+ nsresult error;
+ nsCOMPtr<nsIIdentityCredentialPromptService> icPromptService =
+ mozilla::components::IdentityCredentialPromptService::Service(&error);
+ if (NS_WARN_IF(!icPromptService)) {
+ resultPromise->Reject(error, __func__);
+ return resultPromise;
+ }
+
+ nsCOMPtr<nsIXPConnectWrappedJS> wrapped = do_QueryInterface(icPromptService);
+ AutoJSAPI jsapi;
+ if (NS_WARN_IF(!jsapi.Init(wrapped->GetJSObjectGlobal()))) {
+ resultPromise->Reject(NS_ERROR_FAILURE, __func__);
+ return resultPromise;
+ }
+
+ JS::Rooted<JS::Value> 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<JS::Value> providerJS(jsapi.cx());
+ success = ToJSValue(jsapi.cx(), aProvider, &providerJS);
+ if (NS_WARN_IF(!success)) {
+ resultPromise->Reject(NS_ERROR_FAILURE, __func__);
+ return resultPromise;
+ }
+
+ JS::Rooted<JS::Value> manifestJS(jsapi.cx());
+ success = ToJSValue(jsapi.cx(), aManifest, &manifestJS);
+ if (NS_WARN_IF(!success)) {
+ resultPromise->Reject(NS_ERROR_FAILURE, __func__);
+ return resultPromise;
+ }
+
+ RefPtr<Promise> showPromptPromise;
+ icPromptService->ShowAccountListPrompt(aBrowsingContext, accountsJS,
+ providerJS, manifestJS,
+ getter_AddRefs(showPromptPromise));
+
+ showPromptPromise->AddCallbacksWithCycleCollectedArgs(
+ [aAccounts, resultPromise, aManifest](
+ JSContext*, JS::Handle<JS::Value> 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<JS::Value> 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::GetAccountPromise>
+IdentityCredential::PromptUserWithPolicy(
+ BrowsingContext* aBrowsingContext, nsIPrincipal* aPrincipal,
+ const IdentityProviderAccount& aAccount,
+ const IdentityProviderAPIConfig& aManifest,
+ const IdentityProviderConfig& aProvider) {
+ MOZ_ASSERT(aBrowsingContext);
+ MOZ_ASSERT(aPrincipal);
+
+ nsresult error;
+ nsCOMPtr<nsIIdentityCredentialStorageService> 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<nsIURI> 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<nsIPrincipal> idpPrincipal = BasePrincipal::CreateContentPrincipal(
+ idpURI, aPrincipal->OriginAttributesRef());
+ error = icStorageService->GetState(aPrincipal, idpPrincipal,
+ NS_ConvertUTF16toUTF8(aAccount.mId),
+ &registered, &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> browsingContext(aBrowsingContext);
+ nsCOMPtr<nsIPrincipal> argumentPrincipal(aPrincipal);
+ return FetchMetadata(aPrincipal, aProvider, aManifest)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [aAccount, aManifest, aProvider, argumentPrincipal, browsingContext,
+ icStorageService,
+ idpPrincipal](const IdentityProviderClientMetadata& metadata)
+ -> RefPtr<GenericPromise> {
+ nsresult error;
+ nsCOMPtr<nsIIdentityCredentialPromptService> icPromptService =
+ mozilla::components::IdentityCredentialPromptService::Service(
+ &error);
+ if (NS_WARN_IF(!icPromptService)) {
+ return GenericPromise::CreateAndReject(error, __func__);
+ }
+ nsCOMPtr<nsIXPConnectWrappedJS> wrapped =
+ do_QueryInterface(icPromptService);
+ AutoJSAPI jsapi;
+ if (NS_WARN_IF(!jsapi.Init(wrapped->GetJSObjectGlobal()))) {
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ }
+
+ JS::Rooted<JS::Value> providerJS(jsapi.cx());
+ bool success = ToJSValue(jsapi.cx(), aProvider, &providerJS);
+ if (NS_WARN_IF(!success)) {
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ }
+ JS::Rooted<JS::Value> metadataJS(jsapi.cx());
+ success = ToJSValue(jsapi.cx(), metadata, &metadataJS);
+ if (NS_WARN_IF(!success)) {
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ }
+ JS::Rooted<JS::Value> manifestJS(jsapi.cx());
+ success = ToJSValue(jsapi.cx(), aManifest, &manifestJS);
+ if (NS_WARN_IF(!success)) {
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ }
+
+ RefPtr<Promise> showPromptPromise;
+ icPromptService->ShowPolicyPrompt(
+ browsingContext, providerJS, manifestJS, metadataJS,
+ getter_AddRefs(showPromptPromise));
+
+ RefPtr<GenericPromise::Private> resultPromise =
+ new GenericPromise::Private(__func__);
+ showPromptPromise->AddCallbacksWithCycleCollectedArgs(
+ [aAccount, argumentPrincipal, idpPrincipal, resultPromise,
+ icStorageService](JSContext* aCx, JS::Handle<JS::Value> 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<JS::Value> 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<nsIIdentityCredentialPromptService> icPromptService =
+ mozilla::components::IdentityCredentialPromptService::Service(&error);
+ if (NS_WARN_IF(!icPromptService)) {
+ return;
+ }
+ icPromptService->Close(aBrowsingContext);
+}
+
+// static
+already_AddRefed<Promise> IdentityCredential::LogoutRPs(
+ GlobalObject& aGlobal,
+ const Sequence<IdentityCredentialLogoutRPsRequest>& aLogoutRequests,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ RefPtr<Promise> promise = Promise::CreateResolvedWithUndefined(global, aRv);
+ NS_ENSURE_FALSE(aRv.Failed(), nullptr);
+ nsresult rv;
+ nsCOMPtr<nsIIdentityCredentialStorageService> icStorageService =
+ components::IdentityCredentialStorageService::Service(&rv);
+ if (NS_WARN_IF(!icStorageService)) {
+ aRv.Throw(rv);
+ return nullptr;
+ }
+
+ RefPtr<nsIPrincipal> rpPrincipal = global->PrincipalOrNull();
+ for (const auto& request : aLogoutRequests) {
+ // Get the current state
+ nsCOMPtr<nsIURI> idpURI;
+ rv = NS_NewURI(getter_AddRefs(idpURI), request.mUrl);
+ if (NS_FAILED(rv)) {
+ aRv.ThrowTypeError<MSG_INVALID_URL>(request.mUrl);
+ return nullptr;
+ }
+ nsCOMPtr<nsIPrincipal> idpPrincipal = BasePrincipal::CreateContentPrincipal(
+ idpURI, rpPrincipal->OriginAttributesRef());
+ bool registered, allowLogout;
+ icStorageService->GetState(rpPrincipal, idpPrincipal, request.mAccountId,
+ &registered, &allowLogout);
+
+ // Ignore this request if it isn't permitted
+ if (!(registered && allowLogout)) {
+ continue;
+ }
+
+ // Issue the logout request
+ constexpr auto fragment = ""_ns;
+ auto internalRequest =
+ MakeSafeRefPtr<InternalRequest>(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<Request> domRequest =
+ new Request(global, std::move(internalRequest), nullptr);
+ RequestOrUSVString fetchInput;
+ fetchInput.SetAsRequest() = domRequest;
+ RootedDictionary<RequestInit> requestInit(RootingCx());
+ IgnoredErrorResult error;
+ RefPtr<Promise> 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<RefPtr<T>, 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<RefPtr<IdentityCredential>, nsresult, true>
+ GetIdentityCredentialPromise;
+ typedef MozPromise<IPCIdentityCredential, nsresult, true>
+ GetIPCIdentityCredentialPromise;
+ typedef MozPromise<IdentityProviderConfig, nsresult, true>
+ GetIdentityProviderConfigPromise;
+ typedef MozPromise<bool, nsresult, true> ValidationPromise;
+ typedef MozPromise<IdentityProviderAPIConfig, nsresult, true>
+ GetManifestPromise;
+ typedef std::tuple<IdentityProviderConfig, IdentityProviderAPIConfig>
+ IdentityProviderConfigWithManifest;
+ typedef MozPromise<IdentityProviderConfigWithManifest, nsresult, true>
+ GetIdentityProviderConfigWithManifestPromise;
+ typedef MozPromise<
+ std::tuple<IdentityProviderAPIConfig, IdentityProviderAccountList>,
+ nsresult, true>
+ GetAccountListPromise;
+ typedef MozPromise<std::tuple<IdentityProviderToken, IdentityProviderAccount>,
+ nsresult, true>
+ GetTokenPromise;
+ typedef MozPromise<
+ std::tuple<IdentityProviderAPIConfig, IdentityProviderAccount>, nsresult,
+ true>
+ GetAccountPromise;
+ typedef MozPromise<IdentityProviderClientMetadata, nsresult, true>
+ 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<JSObject*> 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<Promise> LogoutRPs(
+ GlobalObject& aGlobal,
+ const Sequence<IdentityCredentialLogoutRPsRequest>& 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<GetIdentityCredentialPromise> 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<GetIPCIdentityCredentialPromise>
+ 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<GetIPCIdentityCredentialPromise> 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<ValidationPromise> 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<GetManifestPromise> 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<GetAccountListPromise> 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<GetTokenPromise> 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<GetMetadataPromise> 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<GetIdentityProviderConfigWithManifestPromise>
+ PromptUserToSelectProvider(
+ BrowsingContext* aBrowsingContext,
+ const Sequence<IdentityProviderConfig>& aProviders,
+ const Sequence<GetManifestPromise::ResolveOrRejectValue>& 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<GetAccountPromise> 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<GetAccountPromise> 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<mozilla::dom::IdentityProviderConfig> {
+ 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<mozilla::dom::IdentityCredentialRequestOptions> {
+ 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 <typename T, typename TPromise = MozPromise<T, nsresult, true>>
+RefPtr<TPromise> FetchJSONStructure(Request* aRequest) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ // Create the returned Promise
+ RefPtr<typename TPromise::Private> resultPromise =
+ new typename TPromise::Private(__func__);
+
+ // Fetch the provided request
+ RequestOrUSVString fetchInput;
+ fetchInput.SetAsRequest() = aRequest;
+ RootedDictionary<RequestInit> requestInit(RootingCx());
+ IgnoredErrorResult error;
+ RefPtr<Promise> 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<PromiseNativeHandler> reject =
+ new MozPromiseRejectOnDestruction{resultPromise, __func__};
+
+ // Handle the response
+ fetchPromise->AddCallbacksWithCycleCollectedArgs(
+ [resultPromise, reject](JSContext* aCx, JS::Handle<JS::Value> 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<JSObject*> 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<Promise> 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<JS::Value> 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<JS::Value> aValue,
+ ErrorResult&) {
+ resultPromise->Reject(
+ Promise::TryExtractNSResultFromRejectionValue(aValue),
+ __func__);
+ });
+ jsonPromise->AppendNativeHandler(reject);
+ },
+ [resultPromise](JSContext*, JS::Handle<JS::Value> 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 @@
+<!DOCTYPE html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script>
+ window.close();
+</script>
+
+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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Server Error On Accounts Endpoint</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ setupTest("accounts_error").then(
+ function () {
+ return navigator.credentials.get({
+ identity: {
+ providers: [{
+ configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs",
+ clientId: "mochitest",
+ nonce: "nonce"
+ }]
+ }
+ });
+ }
+ ).then((cred) => {
+ ok(false, "incorrectly got a credential");
+ }).catch((err) => {
+ ok(true, "correctly got an error");
+ }).finally(() => {
+ SimpleTest.finish();
+ })
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">This test verifies that we do not get a credential when the accounts endpoint returns an error.</div>
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Server Redirect On Accounts Endpoint</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ setupTest("accounts_redirect").then(
+ function () {
+ return navigator.credentials.get({
+ identity: {
+ providers: [{
+ configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs",
+ clientId: "mochitest",
+ nonce: "nonce"
+ }]
+ }
+ });
+ }
+ ).then((cred) => {
+ ok(false, "incorrectly got a credential");
+ }).catch((err) => {
+ ok(true, "correctly got an error");
+ }).finally(() => {
+ SimpleTest.finish();
+ })
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">This test verifies that we do not get a credential when the accounts endpoint redirects to another (entirely functional) accounts endpoint.</div>
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Delay Reject</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.credentialmanagement.identity.reject_delay.enabled", "true" ],
+ ["dom.security.credentialmanagement.identity.reject_delay.duration_ms", "1000" ],
+ ] })
+ .then(() => {setupTest("delay_reject")})
+ .then(
+ function () {
+ return navigator.credentials.get({
+ identity: {
+ providers: []
+ }
+ });
+ }
+ ).then((cred) => {
+ ok(false, "incorrectly got a credential");
+ }).catch((err) => {
+ ok(true, "correctly got an error");
+ }).finally(() => {
+ SimpleTest.finish();
+ })
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">This test verifies that our rejections are delayed, checking for >500ms.</div>
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Empty Provider List</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ setupTest("empty_provider_list")
+ .then(
+ function () {
+ return navigator.credentials.get({
+ identity: {
+ providers: []
+ }
+ });
+ }
+ ).then((cred) => {
+ ok(false, "incorrectly got a credential");
+ }).catch((err) => {
+ ok(true, "correctly got an error");
+ }).finally(() => {
+ SimpleTest.finish();
+ })
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">This test verifies that we do not get a credential when we give no providers to support.</div>
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>No Providers Specified</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ setupTest("get_without_providers").then(
+ function () {
+ return navigator.credentials.get({
+ identity: {
+ }
+ });
+ }
+ ).then((cred) => {
+ ok(false, "incorrectly got a credential");
+ }).catch((err) => {
+ ok(true, "correctly got an error");
+ }).finally(() => {
+ SimpleTest.finish();
+ })
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">This test verifies that we do not get a credential when we give no providers field in the JSON. This is mostly to make sure we don't have any nullptr derefs.</div>
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Server Error On Token Endpoint</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ setupTest("idtoken_error").then(
+ function () {
+ return navigator.credentials.get({
+ identity: {
+ providers: [{
+ configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs",
+ clientId: "mochitest",
+ nonce: "nonce"
+ }]
+ }
+ });
+ }
+ ).then((cred) => {
+ ok(false, "incorrectly got a credential");
+ }).catch((err) => {
+ ok(true, "correctly got an error");
+ }).finally(() => {
+ SimpleTest.finish();
+ })
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">This test verifies that we do not get a credential when the idtoken endpoint returns an error.</div>
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Server Redirect On Token Endpoint</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ setupTest("idtoken_redirect").then(
+ function () {
+ return navigator.credentials.get({
+ identity: {
+ providers: [{
+ configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs",
+ clientId: "mochitest",
+ nonce: "nonce"
+ }]
+ }
+ });
+ }
+ ).then((cred) => {
+ ok(false, "incorrectly got a credential");
+ }).catch((err) => {
+ ok(true, "correctly got an error");
+ }).finally(() => {
+ SimpleTest.finish();
+ })
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">This test verifies that we do not get a credential when the idtoken endpoint redirects to another (entirely functional) idtoken endpoint.</div>
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Mediation Test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ var err;
+ setupTest("mediation").then(
+ function () {
+ return navigator.credentials.get({
+ identity: {
+ providers: [{
+ configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs",
+ clientId: "mochitest",
+ nonce: "nonce"
+ }]
+ },
+ mediation: "conditional"
+ });
+ }
+ ).catch((e) => {
+ err = e;
+ }).finally(() => {
+ ok(err instanceof TypeError, err.message);
+ SimpleTest.finish();
+ })
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">This verifies that conditional mediation is not supported.</div>
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>No Accounts in the List</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ setupTest("no_accounts").then(
+ function () {
+ return navigator.credentials.get({
+ identity: {
+ providers: [{
+ configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs",
+ clientId: "mochitest",
+ nonce: "nonce"
+ }]
+ }
+ });
+ }
+ ).then((cred) => {
+ ok(false, "incorrectly got a credential");
+ }).catch((err) => {
+ ok(true, "correctly got an error");
+ }).finally(() => {
+ SimpleTest.finish();
+ })
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">This test validates that if a provider returns no accounts, we throw an error from our credential request.</div>
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Happypath Test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ setupTest("simple").then(
+ function () {
+ return navigator.credentials.get({
+ identity: {
+ providers: [{
+ configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs",
+ clientId: "mochitest",
+ nonce: "nonce"
+ }]
+ }
+ });
+ }
+ ).then((cred) => {
+ ok(true, "successfully got a credential");
+ is(cred.token,
+ "account_id=1234&client_id=mochitest&nonce=nonce&disclosure_text_shown=false",
+ "Correct token on the credential.");
+ is(cred.id,
+ "1234",
+ "Correct id on the credential");
+ is(cred.type,
+ "identity",
+ "Correct type on the credential");
+ }).catch((err) => {
+ ok(false, "must not have an error");
+ }).finally(() => {
+ SimpleTest.finish();
+ })
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">This is the main happypath test. We get a credential in a way that should work. This includes simplifying some logic like exactly one account and provider.</div>
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Two Accounts in the List</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ setupTest("two_accounts").then(
+ function () {
+ return navigator.credentials.get({
+ identity: {
+ providers: [{
+ configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs",
+ clientId: "mochitest",
+ nonce: "nonce"
+ }]
+ }
+ });
+ }
+ ).then((cred) => {
+ ok(true, "successfully got a credential");
+ is(cred.token,
+ "account_id=1234&client_id=mochitest&nonce=nonce&disclosure_text_shown=false",
+ "Correct token on the credential.");
+ is(cred.id,
+ "1234",
+ "Correct id on the credential");
+ is(cred.type,
+ "identity",
+ "Correct type on the credential");
+ }).catch((err) => {
+ ok(false, "must not have an error");
+ }).finally(() => {
+ SimpleTest.finish();
+ })
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">This test is temporary until we have an account chooser. It verifies that when we get more than one account from the IDP we just pick the first one.</div>
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Two Providers in a Credential.get()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ setupTest("two_providers").then(
+ function () {
+ return navigator.credentials.get({
+ identity: {
+ providers: [{
+ configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs",
+ clientId: "mochitest",
+ nonce: "nonce"
+ },
+ {
+ configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs",
+ clientId: "mochitest",
+ nonce: "nonce2"
+ }
+ ]
+ }
+ });
+ }
+ ).then((cred) => {
+ ok(true, "successfully got a credential");
+ is(cred.token,
+ "account_id=1234&client_id=mochitest&nonce=nonce&disclosure_text_shown=false",
+ "Correct token on the credential.");
+ is(cred.id,
+ "1234",
+ "Correct id on the credential");
+ is(cred.type,
+ "identity",
+ "Correct type on the credential");
+ }).catch((err) => {
+ ok(false, "must not have an error");
+ }).finally(() => {
+ SimpleTest.finish();
+ })
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Manifest Disagreement</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ setupTest("wrong_provider_in_manifest").then(
+ function () {
+ return navigator.credentials.get({
+ identity: {
+ providers: [{
+ configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest_wrong_provider_in_manifest.sjs",
+ clientId: "mochitest",
+ nonce: "nonce"
+ }]
+ }
+ });
+ }
+ ).then((cred) => {
+ ok(false, "incorrectly got a credential");
+ }).catch((err) => {
+ ok(true, "correctly got an error");
+ }).finally(() => {
+ SimpleTest.finish();
+ })
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">This test is an important privacy check. We make sure the manifest from the argument to credentials.get matches the IDP's root manifest.</div>
+<pre id="test"></pre>
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <script>
+ document.addEventListener("DOMContentLoaded", () => {
+ const frame = document.createElement("frame");
+ document.body.appendChild(frame);
+ const { credentials } = frame.contentWindow.navigator;
+
+ let i = 0;
+ setInterval(async () => {
+ if (i++ > 3) {
+ document.documentElement.removeAttribute("class");
+ }
+ try {
+ await credentials.get({
+ publicKey: {
+ challenge: new Uint8Array(128),
+ allowCredentials: [{ type: 'public-key', id: new TextEncoder().encode('FOOBAR'), }],
+ },
+ });
+ } catch (e) {}
+ frame.remove();
+ }, 1000);
+ }, { once: true });
+ </script>
+</head>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Embedded Frame for Credential Management: Prohibit use in cross-origin iframes</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <meta charset=utf-8>
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+const cose_alg_ECDSA_w_SHA256 = -7;
+var _parentOrigin = "https://example.com/";
+
+function log(msg) {
+ console.log(msg);
+ let logBox = document.getElementById("log");
+ if (logBox) {
+ logBox.textContent += "\n" + msg;
+ }
+}
+
+function local_finished() {
+ parent.postMessage({"done": true}, _parentOrigin);
+ log("Done.");
+}
+
+function local_ok(expression, message) {
+ let body = {"test": expression, "status": expression, "msg": message};
+ parent.postMessage(body, _parentOrigin);
+ log(expression + ": " + message);
+}
+
+function testSameOrigin() {
+ log("Same origin: " + document.domain);
+
+ navigator.credentials.create({publicKey: makeCredentialOptions})
+ .then(function sameOriginCreateThen(aResult) {
+ local_ok(aResult != undefined, "Create worked " + aResult);
+ })
+ .catch(function sameOriginCatch(aResult) {
+ local_ok(false, "Should not have failed " + aResult);
+ })
+ .then(function sameOriginPreventSilentAccess() {
+ return navigator.credentials.preventSilentAccess();
+ })
+ .then(function sameOriginPreventSilentAccessThen(aResult) {
+ local_ok(aResult == undefined, "PreventSilentAccess worked " + aResult);
+ })
+ .catch(function sameOriginPreventSilentAccessCatch(aResult) {
+ local_ok(false, "Should not have failed " + aResult);
+ })
+ .then(function() {
+ local_finished();
+ });
+}
+
+function testCrossOrigin() {
+ log("Cross-origin: " + document.domain);
+
+ navigator.credentials.create({publicKey: makeCredentialOptions})
+ .then(function crossOriginThen(aBad) {
+ local_ok(false, "Should not have succeeded " + aBad);
+ })
+ .catch(function crossOriginCatch(aResult) {
+ local_ok(aResult.toString().startsWith("NotAllowedError"),
+ "Expecting a NotAllowedError, received " + aResult);
+ })
+ .then(function crossOriginPreventSilentAccess() {
+ return navigator.credentials.preventSilentAccess();
+ })
+ .then(function crossOriginPreventSilentAccessThen(aResult) {
+ local_ok(aResult == undefined, "PreventSilentAccess worked " + aResult);
+ })
+ .catch(function crossOriginPreventSilentAccessCatch(aResult) {
+ local_ok(false, "Should not have failed " + aResult);
+ })
+ .then(function() {
+ local_finished();
+ });
+}
+
+let rp = {id: document.domain, name: "none", icon: "none"};
+let user = {
+ id: crypto.getRandomValues(new Uint8Array(16)),
+ name: "none", icon: "none", displayName: "none",
+};
+let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256};
+let makeCredentialOptions = {
+ rp, user, challenge: new Uint8Array(), pubKeyCredParams: [param],
+};
+
+if (document.domain == "example.com") {
+ testSameOrigin();
+} else {
+ testCrossOrigin();
+}
+
+</script>
+
+<div id="log"></div>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<head>
+ <title>Credential Management: Handle requests with empty options</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <meta charset=utf-8>
+</head>
+<body>
+<h1>Credential Management: Handle requests with empty options</h1>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({"set": [["security.webauth.webauthn", true],
+ ["dom.security.credentialmanagement.enabled", true]
+ ]},
+async function() {
+ info("testing create({}).")
+ try {
+ await navigator.credentials.create({});
+ ok(false, "Credential creation with no options should be an error.");
+ }
+ catch (err) {
+ is(err.name, "NotSupportedError", "Credential creation with no options is a NotSupportedError");
+ }
+ info("testing get({}).")
+ try {
+ await navigator.credentials.get({});
+ ok(false, "Credential get with no options should be an error.");
+ }
+ catch (err) {
+ is(err.name, "NotSupportedError", "Credential get with no options is a NotSupportedError");
+ }
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<head>
+ <title>Credential Management: Prohibit use in cross-origin iframes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <meta charset=utf-8>
+</head>
+<body>
+<h1>Credential Management: Prohibit use in cross-origin iframes</h1>
+<ul>
+ <li><a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1407789">Mozilla Bug 1407789</a></li>
+</ul>
+
+<div id="framediv">
+ <h2>Same Origin Test</h2>
+ <iframe id="frame_top"></iframe>
+
+ <h2>Cross-Origin Test</h2>
+ <iframe id="frame_bottom"></iframe>
+</div>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+var _countCompletes = 0;
+var _expectedCompletes = 2; // 2 iframes
+
+var _done = new Promise((resolve) => {
+ function handleEventMessage(event) {
+ if ("test" in event.data) {
+ let summary = event.data.test + ": " + event.data.msg;
+ ok(event.data.status, summary);
+ } else if ("done" in event.data) {
+ _countCompletes += 1;
+ if (_countCompletes == _expectedCompletes) {
+ console.log("Test compeleted. Finished.");
+ resolve();
+ }
+ } else {
+ ok(false, "Unexpected message in the test harness: " + event.data);
+ }
+ }
+
+ window.addEventListener("message", handleEventMessage);
+});
+
+async function addVirtualAuthenticator() {
+ let id = await SpecialPowers.spawnChrome([], () => {
+ let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
+ Ci.nsIWebAuthnService
+ );
+ return webauthnService.addVirtualAuthenticator(
+ "ctap2",
+ "internal",
+ true,
+ true,
+ true,
+ true
+ );
+ });
+
+ SimpleTest.registerCleanupFunction(async () => {
+ await SpecialPowers.spawnChrome([id], (authenticatorId) => {
+ let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
+ Ci.nsIWebAuthnService
+ );
+ webauthnService.removeVirtualAuthenticator(authenticatorId);
+ });
+ });
+}
+
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({"set": [["security.webauth.webauthn", true],
+ ["security.webauth.webauthn_enable_softtoken", true],
+ ["security.webauth.webauthn_enable_usbtoken", false]]});
+ await addVirtualAuthenticator();
+});
+
+add_task(async () => {
+ document.getElementById("frame_top").src = "https://example.com/tests/dom/credentialmanagement/tests/mochitest/frame_credman_iframes.html";
+
+ document.getElementById("frame_bottom").src = "https://test1.example.com/tests/dom/credentialmanagement/tests/mochitest/frame_credman_iframes.html";
+
+ await _done;
+});
+</script>
+</body>
+</html>