Bug 1415675 - Web Authentication - Support AbortSignal types r=jcj,smaug

Summary:
This patch adds support for aborting WebAuthn requests via AbortSignals.

https://w3c.github.io/webauthn/#abortoperation
https://w3c.github.io/webauthn/#sample-aborting
https://dom.spec.whatwg.org/#abortcontroller-api-integration

It also adds a variety of request abortion/cancellation tests.

To test request cancellation we can use USB tokens as those requests will
never complete without a token and/or user interaction. A bonus here is that
we'll have a little coverage for u2f-hid-rs.

Reviewers: jcj, smaug

Reviewed By: jcj, smaug

Bug #: 1415675

Differential Revision: https://phabricator.services.mozilla.com/D245

--HG--
extra : amend_source : bd779d5c4c6a11dd8ce34c0cc86675825b799031
This commit is contained in:
Tim Taubert 2017-11-17 09:44:50 +01:00
Родитель bfd41fe2bc
Коммит 73cfd2472a
10 изменённых файлов: 432 добавлений и 9 удалений

Просмотреть файл

@ -38,14 +38,14 @@ already_AddRefed<Promise>
CredentialsContainer::Get(const CredentialRequestOptions& aOptions)
{
RefPtr<WebAuthnManager> mgr = WebAuthnManager::GetOrCreate();
return mgr->GetAssertion(mParent, aOptions.mPublicKey);
return mgr->GetAssertion(mParent, aOptions.mPublicKey, aOptions.mSignal);
}
already_AddRefed<Promise>
CredentialsContainer::Create(const CredentialCreationOptions& aOptions)
{
RefPtr<WebAuthnManager> mgr = WebAuthnManager::GetOrCreate();
return mgr->MakeCredential(mParent, aOptions.mPublicKey);
return mgr->MakeCredential(mParent, aOptions.mPublicKey, aOptions.mSignal);
}
already_AddRefed<Promise>

Просмотреть файл

@ -199,6 +199,7 @@ WebAuthnManager::ClearTransaction()
}
mTransaction.reset();
Unfollow();
}
void
@ -289,7 +290,8 @@ WebAuthnManager::Get()
already_AddRefed<Promise>
WebAuthnManager::MakeCredential(nsPIDOMWindowInner* aParent,
const MakePublicKeyCredentialOptions& aOptions)
const MakePublicKeyCredentialOptions& aOptions,
const Optional<OwningNonNull<AbortSignal>>& aSignal)
{
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aParent);
@ -306,6 +308,12 @@ WebAuthnManager::MakeCredential(nsPIDOMWindowInner* aParent,
return nullptr;
}
// Abort the request if aborted flag is already set.
if (aSignal.WasPassed() && aSignal.Value().Aborted()) {
promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
return promise.forget();
}
nsString origin;
nsCString rpId;
rv = GetOrigin(aParent, origin, rpId);
@ -475,11 +483,18 @@ WebAuthnManager::MakeCredential(nsPIDOMWindowInner* aParent,
ListenForVisibilityEvents(aParent, this);
AbortSignal* signal = nullptr;
if (aSignal.WasPassed()) {
signal = &aSignal.Value();
Follow(signal);
}
MOZ_ASSERT(mTransaction.isNothing());
mTransaction = Some(WebAuthnTransaction(aParent,
promise,
Move(info),
Move(clientDataJSON)));
Move(clientDataJSON),
signal));
mChild->SendRequestRegister(mTransaction.ref().mId, mTransaction.ref().mInfo);
return promise.forget();
@ -487,7 +502,8 @@ WebAuthnManager::MakeCredential(nsPIDOMWindowInner* aParent,
already_AddRefed<Promise>
WebAuthnManager::GetAssertion(nsPIDOMWindowInner* aParent,
const PublicKeyCredentialRequestOptions& aOptions)
const PublicKeyCredentialRequestOptions& aOptions,
const Optional<OwningNonNull<AbortSignal>>& aSignal)
{
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aParent);
@ -504,6 +520,12 @@ WebAuthnManager::GetAssertion(nsPIDOMWindowInner* aParent,
return nullptr;
}
// Abort the request if aborted flag is already set.
if (aSignal.WasPassed() && aSignal.Value().Aborted()) {
promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
return promise.forget();
}
nsString origin;
nsCString rpId;
rv = GetOrigin(aParent, origin, rpId);
@ -624,11 +646,18 @@ WebAuthnManager::GetAssertion(nsPIDOMWindowInner* aParent,
ListenForVisibilityEvents(aParent, this);
AbortSignal* signal = nullptr;
if (aSignal.WasPassed()) {
signal = &aSignal.Value();
Follow(signal);
}
MOZ_ASSERT(mTransaction.isNothing());
mTransaction = Some(WebAuthnTransaction(aParent,
promise,
Move(info),
Move(clientDataJSON)));
Move(clientDataJSON),
signal));
mChild->SendRequestSign(mTransaction.ref().mId, mTransaction.ref().mInfo);
return promise.forget();
@ -910,6 +939,12 @@ WebAuthnManager::HandleEvent(nsIDOMEvent* aEvent)
return NS_OK;
}
void
WebAuthnManager::Abort()
{
CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
}
void
WebAuthnManager::ActorDestroyed()
{

Просмотреть файл

@ -66,11 +66,13 @@ public:
WebAuthnTransaction(nsPIDOMWindowInner* aParent,
const RefPtr<Promise>& aPromise,
const WebAuthnTransactionInfo&& aInfo,
const nsAutoCString&& aClientData)
const nsAutoCString&& aClientData,
AbortSignal* aSignal)
: mParent(aParent)
, mPromise(aPromise)
, mInfo(aInfo)
, mClientData(aClientData)
, mSignal(aSignal)
, mId(NextId())
{
MOZ_ASSERT(mId > 0);
@ -89,6 +91,9 @@ public:
// Client data used to assemble reply objects.
nsCString mClientData;
// An optional AbortSignal instance.
RefPtr<AbortSignal> mSignal;
// Unique transaction id.
uint64_t mId;
@ -103,6 +108,7 @@ private:
};
class WebAuthnManager final : public nsIDOMEventListener
, public AbortFollower
{
public:
NS_DECL_ISUPPORTS
@ -113,11 +119,13 @@ public:
already_AddRefed<Promise>
MakeCredential(nsPIDOMWindowInner* aParent,
const MakePublicKeyCredentialOptions& aOptions);
const MakePublicKeyCredentialOptions& aOptions,
const Optional<OwningNonNull<AbortSignal>>& aSignal);
already_AddRefed<Promise>
GetAssertion(nsPIDOMWindowInner* aParent,
const PublicKeyCredentialRequestOptions& aOptions);
const PublicKeyCredentialRequestOptions& aOptions,
const Optional<OwningNonNull<AbortSignal>>& aSignal);
already_AddRefed<Promise>
Store(nsPIDOMWindowInner* aParent, const Credential& aCredential);
@ -134,6 +142,8 @@ public:
void
RequestAborted(const uint64_t& aTransactionId, const nsresult& aError);
void Abort() override;
void ActorDestroyed();
private:

Просмотреть файл

@ -1,9 +1,11 @@
[DEFAULT]
support-files =
tab_webauthn_result.html
tab_webauthn_success.html
../cbor/*
../pkijs/*
../u2futil.js
skip-if = !e10s
[browser_abort_visibility.js]
[browser_webauthn_telemetry.js]

Просмотреть файл

@ -0,0 +1,113 @@
/* 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/browser/dom/webauthn/tests/browser/tab_webauthn_result.html";
async function assertStatus(tab, expected) {
let actual = await ContentTask.spawn(tab.linkedBrowser, null, async function () {
return content.document.getElementById("status").value;
});
is(actual, expected, "webauthn request " + expected);
}
async function waitForStatus(tab, expected) {
await ContentTask.spawn(tab.linkedBrowser, [expected], async function (expected) {
return ContentTaskUtils.waitForCondition(() => {
return content.document.getElementById("status").value == expected;
});
});
await assertStatus(tab, expected);
}
function startMakeCredentialRequest(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}],
};
let status = content.document.getElementById("status");
content.navigator.credentials.create({publicKey}).then(() => {
status.value = "completed";
}).catch(() => {
status.value = "aborted";
});
status.value = "pending";
});
}
function startGetAssertionRequest(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]
};
let status = content.document.getElementById("status");
content.navigator.credentials.get({publicKey}).then(() => {
status.value = "completed";
}).catch(() => {
status.value = "aborted";
});
status.value = "pending";
});
}
// Test that MakeCredential() and GetAssertion() requests
// are aborted when the current tab loses its focus.
add_task(async function test_abort() {
// Enable the USB token.
Services.prefs.setBoolPref("security.webauth.webauthn", true);
Services.prefs.setBoolPref("security.webauth.webauthn_enable_softtoken", false);
Services.prefs.setBoolPref("security.webauth.webauthn_enable_usbtoken", true);
// Create a new tab for the MakeCredential() request.
let tab_create = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
// Start the request.
await startMakeCredentialRequest(tab_create);
await assertStatus(tab_create, "pending");
// Open another tab and switch to it. The first will lose focus.
let tab_get = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
await waitForStatus(tab_create, "aborted");
// Start a GetAssertion() request in the second tab.
await startGetAssertionRequest(tab_get);
await assertStatus(tab_get, "pending");
// Switch back to the first tab, the get() request is aborted.
await BrowserTestUtils.switchTab(gBrowser, tab_create);
await waitForStatus(tab_get, "aborted");
// Close tabs.
await BrowserTestUtils.removeTab(tab_create);
await BrowserTestUtils.removeTab(tab_get);
// Cleanup.
Services.prefs.clearUserPref("security.webauth.webauthn");
Services.prefs.clearUserPref("security.webauth.webauthn_enable_softtoken");
Services.prefs.clearUserPref("security.webauth.webauthn_enable_usbtoken");
});

Просмотреть файл

@ -0,0 +1,14 @@
<!DOCTYPE html>
<meta charset=utf-8>
<head>
<title>Generic W3C Web Authentication Test Result Page</title>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<h1>Generic W3C Web Authentication Test Result Page</h1>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1415675">Mozilla Bug 1415675</a>
<input type="text" id="status" value="init" />
</body>
</html>

Просмотреть файл

@ -6,10 +6,12 @@ support-files =
skip-if = !e10s
scheme = https
[test_webauthn_abort_signal.html]
[test_webauthn_loopback.html]
[test_webauthn_no_token.html]
[test_webauthn_make_credential.html]
[test_webauthn_get_assertion.html]
[test_webauthn_override_request.html]
[test_webauthn_store_credential.html]
[test_webauthn_sameorigin.html]
[test_webauthn_isplatformauthenticatoravailable.html]

Просмотреть файл

@ -0,0 +1,148 @@
<!DOCTYPE html>
<meta charset=utf-8>
<head>
<title>Test for aborting W3C Web Authentication request</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
<script type="text/javascript" src="u2futil.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<h1>Test for aborting W3C Web Authentication request</h1>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1415675">Mozilla Bug 1415675</a>
<script class="testbody" type="text/javascript">
"use strict";
function arrivingHereIsBad(aResult) {
ok(false, "Bad result! Received a: " + aResult);
}
function expectAbortError(aResult) {
is(aResult.code, DOMException.ABORT_ERR, "Expecting an AbortError");
}
add_task(() => {
// Enable USB tokens.
return SpecialPowers.pushPrefEnv({"set": [
["security.webauth.webauthn", true],
["security.webauth.webauthn_enable_softtoken", false],
["security.webauth.webauthn_enable_usbtoken", true],
]});
});
// Start a new MakeCredential() request.
function requestMakeCredential(signal) {
let publicKey = {
rp: {id: document.domain, name: "none", icon: "none"},
user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
challenge: 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 navigator.credentials.create({publicKey, signal});
}
// Start a new GetAssertion() request.
async function requestGetAssertion(signal) {
let newCredential = {
type: "public-key",
id: crypto.getRandomValues(new Uint8Array(16)),
transports: ["usb"],
};
let publicKey = {
challenge: crypto.getRandomValues(new Uint8Array(16)),
timeout: 5000, // the minimum timeout is actually 15 seconds
rpId: document.domain,
allowCredentials: [newCredential]
};
// Start the request, handle failures only.
return navigator.credentials.get({publicKey, signal});
}
// Create an AbortController and abort immediately.
add_task(async () => {
let ctrl = new AbortController();
ctrl.abort();
// The event shouldn't fire.
ctrl.signal.onabort = arrivingHereIsBad;
// MakeCredential() should abort immediately.
await requestMakeCredential(ctrl.signal)
.then(arrivingHereIsBad)
.catch(expectAbortError);
// GetAssertion() should abort immediately.
await requestGetAssertion(ctrl.signal)
.then(arrivingHereIsBad)
.catch(expectAbortError);
});
// Request a new credential and abort the request.
add_task(async () => {
let aborted = false;
let ctrl = new AbortController();
ctrl.signal.onabort = () => {
ok(!aborted, "abort event fired once");
aborted = true;
};
// Request a new credential.
let request = requestMakeCredential(ctrl.signal)
.then(arrivingHereIsBad)
.catch(err => {
ok(aborted, "abort event was fired");
expectAbortError(err);
});
// Wait a tick for the statemachine to start.
await Promise.resolve();
// Abort the request.
ok(!aborted, "request still pending");
ctrl.abort();
ok(aborted, "request aborted");
// Wait for the request to terminate.
await request;
});
// Request a new assertion and abort the request.
add_task(async () => {
let aborted = false;
let ctrl = new AbortController();
ctrl.signal.onabort = () => {
ok(!aborted, "abort event fired once");
aborted = true;
};
// Request a new assertion.
let request = requestGetAssertion(ctrl.signal)
.then(arrivingHereIsBad)
.catch(err => {
ok(aborted, "abort event was fired");
expectAbortError(err);
});
// Wait a tick for the statemachine to start.
await Promise.resolve();
// Abort the request.
ok(!aborted, "request still pending");
ctrl.abort();
ok(aborted, "request aborted");
// Wait for the request to terminate.
await request;
});
</script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,97 @@
<!DOCTYPE html>
<meta charset=utf-8>
<head>
<title>Test for overriding W3C Web Authentication request</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
<script type="text/javascript" src="u2futil.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<h1>Test for overriding W3C Web Authentication request</h1>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1415675">Mozilla Bug 1415675</a>
<script class="testbody" type="text/javascript">
"use strict";
// Last request status.
let status = "";
add_task(() => {
// Enable USB tokens.
return SpecialPowers.pushPrefEnv({"set": [
["security.webauth.webauthn", true],
["security.webauth.webauthn_enable_softtoken", false],
["security.webauth.webauthn_enable_usbtoken", true],
]});
});
// Start a new MakeCredential() request.
async function requestMakeCredential(status_value) {
let publicKey = {
rp: {id: document.domain, name: "none", icon: "none"},
user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
challenge: crypto.getRandomValues(new Uint8Array(16)),
timeout: 5000, // the minimum timeout is actually 15 seconds
pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}],
};
// Start the request, handle failures only.
navigator.credentials.create({publicKey}).catch(() => {
status = status_value;
});
// Wait a tick to let the statemachine start.
await Promise.resolve();
}
// Start a new GetAssertion() request.
async function requestGetAssertion(status_value) {
let newCredential = {
type: "public-key",
id: crypto.getRandomValues(new Uint8Array(16)),
transports: ["usb"],
};
let publicKey = {
challenge: crypto.getRandomValues(new Uint8Array(16)),
timeout: 5000, // the minimum timeout is actually 15 seconds
rpId: document.domain,
allowCredentials: [newCredential]
};
// Start the request, handle failures only.
navigator.credentials.get({publicKey}).catch(() => {
status = status_value;
});
// Wait a tick to let the statemachine start.
await Promise.resolve();
}
// Test that .create() and .get() requests override any pending requests.
add_task(async () => {
// Request a new credential.
await requestMakeCredential("aborted1");
// Request another credential, the first request will abort.
await requestMakeCredential("aborted2");
is(status, "aborted1", "first request aborted");
// Request an assertion, the second request will abort.
await requestGetAssertion("aborted3");
is(status, "aborted2", "second request aborted");
// Request another assertion, the third request will abort.
await requestGetAssertion("aborted4");
is(status, "aborted3", "third request aborted");
// Request another credential, the fourth request will abort.
await requestMakeCredential("aborted5");
is(status, "aborted4", "fourth request aborted");
});
</script>
</body>
</html>

Просмотреть файл

@ -22,8 +22,10 @@ interface CredentialsContainer {
dictionary CredentialRequestOptions {
PublicKeyCredentialRequestOptions publicKey;
AbortSignal signal;
};
dictionary CredentialCreationOptions {
MakePublicKeyCredentialOptions publicKey;
AbortSignal signal;
};