Bug 1383799 - Cancel WebAuthn operations on tab-switch r?ttaubert draft
authorJ.C. Jones <jjones@mozilla.com>
Fri, 04 Aug 2017 12:34:18 -0700
changeset 643538 558e26c6a8e20e243970e368ee57752f983e96c3
parent 620494 fa1da3c0b200abbd9cfab3cab19962824314044e
child 725328 a8d3afb18b84a806a645d26ed5f2917875f3b7f0
push id73128
push userbmo:jjones@mozilla.com
push dateWed, 09 Aug 2017 20:56:45 +0000
reviewersttaubert
bugs1383799
milestone57.0a1
Bug 1383799 - Cancel WebAuthn operations on tab-switch r?ttaubert WebAuthn operations that are in-flight with authenticators must be cancelled when switching tabs. There's an Issue [1] opened with the WebAuthn spec for this already, but the language is _not_ in spec. Still, it's necessary for security, spec or not. This also matches how Chromium handles U2F operations during a tab switch. [1] https://github.com/w3c/webauthn/issues/316 MozReview-Commit-ID: 6Qh9oC4pqys
dom/webauthn/WebAuthnManager.cpp
dom/webauthn/WebAuthnManager.h
--- a/dom/webauthn/WebAuthnManager.cpp
+++ b/dom/webauthn/WebAuthnManager.cpp
@@ -37,17 +37,20 @@ const uint8_t FLAG_AT = 0x40; // Authent
  * Statics
  **********************************************************************/
 
 namespace {
 StaticRefPtr<WebAuthnManager> gWebAuthnManager;
 static mozilla::LazyLogModule gWebAuthnManagerLog("webauthnmanager");
 }
 
-NS_IMPL_ISUPPORTS(WebAuthnManager, nsIIPCBackgroundChildCreateCallback);
+NS_NAMED_LITERAL_STRING(kVisibilityChange, "visibilitychange");
+
+NS_IMPL_ISUPPORTS(WebAuthnManager, nsIIPCBackgroundChildCreateCallback,
+                  nsIDOMEventListener);
 
 /***********************************************************************
  * Utility Functions
  **********************************************************************/
 
 template<class OOS>
 static nsresult
 GetAlgorithmName(const OOS& aAlgorithm,
@@ -126,16 +129,17 @@ AssembleClientData(const nsAString& aOri
   aJsonOut.Assign(NS_ConvertUTF16toUTF8(temp));
   return NS_OK;
 }
 
 nsresult
 GetOrigin(nsPIDOMWindowInner* aParent,
           /*out*/ nsAString& aOrigin, /*out*/ nsACString& aHost)
 {
+  MOZ_ASSERT(aParent);
   nsCOMPtr<nsIDocument> doc = aParent->GetDoc();
   MOZ_ASSERT(doc);
 
   nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
   nsresult rv = nsContentUtils::GetUTFOrigin(principal, aOrigin);
   if (NS_WARN_IF(NS_FAILED(rv)) ||
       NS_WARN_IF(aOrigin.IsEmpty())) {
     return NS_ERROR_FAILURE;
@@ -163,16 +167,17 @@ GetOrigin(nsPIDOMWindowInner* aParent,
 nsresult
 RelaxSameOrigin(nsPIDOMWindowInner* aParent,
                 const nsAString& aInputRpId,
                 /* out */ nsACString& aRelaxedRpId)
 {
   MOZ_ASSERT(aParent);
   nsCOMPtr<nsIDocument> doc = aParent->GetDoc();
   MOZ_ASSERT(doc);
+
   nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
   nsCOMPtr<nsIURI> uri;
   if (NS_FAILED(principal->GetURI(getter_AddRefs(uri)))) {
     return NS_ERROR_FAILURE;
   }
   nsAutoCString originHost;
   if (NS_FAILED(uri->GetAsciiHost(originHost))) {
     return NS_ERROR_FAILURE;
@@ -207,31 +212,71 @@ RelaxSameOrigin(nsPIDOMWindowInner* aPar
   if (!html->IsRegistrableDomainSuffixOfOrEqualTo(inputRpId, originHost)) {
     return NS_ERROR_DOM_SECURITY_ERR;
   }
 
   aRelaxedRpId.Assign(NS_ConvertUTF16toUTF8(aInputRpId));
   return NS_OK;
 }
 
+static void
+ListenForVisibilityEvents(nsPIDOMWindowInner* aParent,
+                          WebAuthnManager* aListener)
+{
+  MOZ_ASSERT(aParent);
+  MOZ_ASSERT(aListener);
+
+  nsCOMPtr<nsIDocument> doc = aParent->GetExtantDoc();
+  if (NS_WARN_IF(!doc)) {
+    return;
+  }
+
+  nsresult rv = doc->AddSystemEventListener(kVisibilityChange, aListener,
+                                            /* use capture */ true,
+                                            /* wants untrusted */ false);
+  Unused << NS_WARN_IF(NS_FAILED(rv));
+}
+
+static void
+StopListeningForVisibilityEvents(nsPIDOMWindowInner* aParent,
+                                 WebAuthnManager* aListener)
+{
+  MOZ_ASSERT(aParent);
+  MOZ_ASSERT(aListener);
+
+  nsCOMPtr<nsIDocument> doc = aParent->GetExtantDoc();
+  if (NS_WARN_IF(!doc)) {
+    return;
+  }
+
+  nsresult rv = doc->RemoveSystemEventListener(kVisibilityChange, aListener,
+                                               /* use capture */ true);
+  Unused << NS_WARN_IF(NS_FAILED(rv));
+}
+
 /***********************************************************************
  * WebAuthnManager Implementation
  **********************************************************************/
 
 WebAuthnManager::WebAuthnManager()
 {
   MOZ_ASSERT(NS_IsMainThread());
 }
 
 void
 WebAuthnManager::MaybeClearTransaction()
 {
   mClientData.reset();
   mInfo.reset();
   mTransactionPromise = nullptr;
+  if (mCurrentParent) {
+    StopListeningForVisibilityEvents(mCurrentParent, this);
+    mCurrentParent = nullptr;
+  }
+
   if (mChild) {
     RefPtr<WebAuthnTransactionChild> c;
     mChild.swap(c);
     c->Send__delete__(c);
   }
 }
 
 WebAuthnManager::~WebAuthnManager()
@@ -498,16 +543,18 @@ WebAuthnManager::MakeCredential(nsPIDOMW
           []() {
             // This case can't actually happen, we'll have crashed if the child
             // failed to create.
           });
   mTransactionPromise = promise;
   mClientData = Some(clientDataJSON);
   mCurrentParent = aParent;
   mInfo = Some(info);
+  ListenForVisibilityEvents(aParent, this);
+
   return promise.forget();
 }
 
 void
 WebAuthnManager::StartRegister() {
   if (mChild) {
     mChild->SendRequestRegister(mInfo.ref());
   }
@@ -515,16 +562,23 @@ WebAuthnManager::StartRegister() {
 
 void
 WebAuthnManager::StartSign() {
   if (mChild) {
     mChild->SendRequestSign(mInfo.ref());
   }
 }
 
+void
+WebAuthnManager::StartCancel() {
+  if (mChild) {
+    mChild->SendRequestCancel();
+  }
+}
+
 already_AddRefed<Promise>
 WebAuthnManager::GetAssertion(nsPIDOMWindowInner* aParent,
                               const PublicKeyCredentialRequestOptions& aOptions)
 {
   MOZ_ASSERT(aParent);
 
   MaybeClearTransaction();
 
@@ -664,16 +718,18 @@ WebAuthnManager::GetAssertion(nsPIDOMWin
             // failed to create.
           });
 
   // Only store off the promise if we've succeeded in sending the IPC event.
   mTransactionPromise = promise;
   mClientData = Some(clientDataJSON);
   mCurrentParent = aParent;
   mInfo = Some(info);
+  ListenForVisibilityEvents(aParent, this);
+
   return promise.forget();
 }
 
 void
 WebAuthnManager::FinishMakeCredential(nsTArray<uint8_t>& aRegBuffer)
 {
   MOZ_ASSERT(mTransactionPromise);
   MOZ_ASSERT(mInfo.isSome());
@@ -872,22 +928,51 @@ WebAuthnManager::FinishGetAssertion(nsTA
 
   mTransactionPromise->MaybeResolve(credential);
   MaybeClearTransaction();
 }
 
 void
 WebAuthnManager::Cancel(const nsresult& aError)
 {
+  MOZ_ASSERT(NS_IsMainThread());
+
   if (mTransactionPromise) {
     mTransactionPromise->MaybeReject(aError);
   }
+
   MaybeClearTransaction();
 }
 
+NS_IMETHODIMP
+WebAuthnManager::HandleEvent(nsIDOMEvent* aEvent)
+{
+  MOZ_ASSERT(aEvent);
+
+  nsAutoString type;
+  aEvent->GetType(type);
+  if (!type.Equals(kVisibilityChange)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsCOMPtr<nsIDocument> doc =
+    do_QueryInterface(aEvent->InternalDOMEvent()->GetTarget());
+  MOZ_ASSERT(doc);
+
+  if (doc && doc->Hidden()) {
+    MOZ_LOG(gWebAuthnManagerLog, LogLevel::Debug,
+            ("Visibility change: WebAuthn window is hidden, cancelling job."));
+
+    StartCancel();
+    Cancel(NS_ERROR_ABORT);
+  }
+
+  return NS_OK;
+}
+
 void
 WebAuthnManager::ActorCreated(PBackgroundChild* aActor)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aActor);
 
   if (mChild) {
     return;
--- a/dom/webauthn/WebAuthnManager.h
+++ b/dom/webauthn/WebAuthnManager.h
@@ -3,17 +3,19 @@
 /* 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_WebAuthnManager_h
 #define mozilla_dom_WebAuthnManager_h
 
 #include "mozilla/MozPromise.h"
+#include "mozilla/dom/Event.h"
 #include "mozilla/dom/PWebAuthnTransaction.h"
+#include "nsIDOMEventListener.h"
 #include "nsIIPCBackgroundChildCreateCallback.h"
 
 /*
  * Content process manager for the WebAuthn protocol. Created on calls to the
  * WebAuthentication DOM object, this manager handles establishing IPC channels
  * for WebAuthn transactions, as well as keeping track of JS Promise objects
  * representing transactions in flight.
  *
@@ -56,20 +58,22 @@ class ArrayBufferViewOrArrayBuffer;
 struct AssertionOptions;
 class OwningArrayBufferViewOrArrayBuffer;
 struct ScopedCredentialOptions;
 struct ScopedCredentialParameters;
 class Promise;
 class WebAuthnTransactionChild;
 class WebAuthnTransactionInfo;
 
-class WebAuthnManager final : public nsIIPCBackgroundChildCreateCallback
+class WebAuthnManager final : public nsIIPCBackgroundChildCreateCallback,
+                              public nsIDOMEventListener
 {
 public:
   NS_DECL_ISUPPORTS
+  NS_DECL_NSIDOMEVENTLISTENER
   static WebAuthnManager* GetOrCreate();
   static WebAuthnManager* Get();
 
   void
   FinishMakeCredential(nsTArray<uint8_t>& aRegBuffer);
 
   void
   FinishGetAssertion(nsTArray<uint8_t>& aCredentialId,
@@ -83,16 +87,17 @@ public:
                  const MakeCredentialOptions& aOptions);
 
   already_AddRefed<Promise>
   GetAssertion(nsPIDOMWindowInner* aParent,
                const PublicKeyCredentialRequestOptions& aOptions);
 
   void StartRegister();
   void StartSign();
+  void StartCancel();
 
   // nsIIPCbackgroundChildCreateCallback methods
   void ActorCreated(PBackgroundChild* aActor) override;
   void ActorFailed() override;
   void ActorDestroyed();
 private:
   WebAuthnManager();
   virtual ~WebAuthnManager();