Merge fix/orchestrator/passkey-single-flight into master
This commit is contained in:
commit
23f10bce68
2 files changed
+83
-1
No files matched your search
@@ -32,7 +32,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function signedProof(path, fields, label) {
|
let signing = false;
|
||||||
|
|
||||||
|
async function prove(path, fields, label) {
|
||||||
const result = await sendJson(path, fields);
|
const result = await sendJson(path, fields);
|
||||||
if (!result.ok || !result.data) {
|
if (!result.ok || !result.data) {
|
||||||
toast(`${label}: not possible`, fail(result), false);
|
toast(`${label}: not possible`, fail(result), false);
|
||||||
@@ -45,6 +47,19 @@
|
|||||||
return signed ? {proof: {payload: result.data.payload, assertion: signed}} : null;
|
return signed ? {proof: {payload: result.data.payload, assertion: signed}} : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function signedProof(path, fields, label) {
|
||||||
|
if (signing) {
|
||||||
|
toast(`${label}: not signed`, ["Another passkey signature is still open: finish or cancel it first; nothing was sent."], false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
signing = true;
|
||||||
|
try {
|
||||||
|
return await prove(path, fields, label);
|
||||||
|
} finally {
|
||||||
|
signing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function proofFor(entries, label) {
|
function proofFor(entries, label) {
|
||||||
return signedProof("/api/sign", {items: entries}, label);
|
return signedProof("/api/sign", {items: entries}, label);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from hub_support import HUB
|
||||||
|
|
||||||
|
PASSKEY = HUB / "templates" / "page-passkey.js"
|
||||||
|
PROBE = """
|
||||||
|
const source = require("fs").readFileSync(process.argv[1], "utf8");
|
||||||
|
const toasts = [];
|
||||||
|
const gates = [];
|
||||||
|
let asked = 0;
|
||||||
|
let served = 0;
|
||||||
|
const window = {location: {hostname: "localhost", port: "8737", pathname: "/", hash: ""}};
|
||||||
|
const navigator = {credentials: {get: () => {
|
||||||
|
asked += 1;
|
||||||
|
return new Promise((resolve, reject) => gates.push({resolve, reject}));
|
||||||
|
}}};
|
||||||
|
const sendJson = async () => {
|
||||||
|
served += 1;
|
||||||
|
return {ok: true, data: {payload: {nonce: served}, challenge: "AAAA", credentials: ["AAAA"]}};
|
||||||
|
};
|
||||||
|
const toast = (title) => toasts.push(title);
|
||||||
|
const bytes = new Uint8Array([1]).buffer;
|
||||||
|
const credential = {rawId: bytes, response: {clientDataJSON: bytes, authenticatorData: bytes, signature: bytes}};
|
||||||
|
const P = new Function("window", "navigator", "sendJson", "toast", "fail", "h", `${source}\\nreturn {signedProof};`)(window, navigator, sendJson, toast, () => [], () => null);
|
||||||
|
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
||||||
|
const nonce = (signed) => (signed ? signed.proof.payload.nonce : null);
|
||||||
|
(async () => {
|
||||||
|
const first = P.signedProof("/api/board/sign", {action: "resume"}, "Resume");
|
||||||
|
const second = P.signedProof("/api/board/sign", {action: "resume"}, "Resume");
|
||||||
|
await tick();
|
||||||
|
const open = {asked, served, toasts: [...toasts]};
|
||||||
|
gates.splice(0).forEach((gate) => gate.resolve(credential));
|
||||||
|
const both = [nonce(await first), nonce(await second)];
|
||||||
|
const canceled = P.signedProof("/api/board/sign", {action: "drain"}, "Drain");
|
||||||
|
await tick();
|
||||||
|
gates.splice(0).forEach((gate) => gate.reject(new Error("canceled")));
|
||||||
|
const dropped = nonce(await canceled);
|
||||||
|
const after = P.signedProof("/api/board/sign", {action: "suspend"}, "Suspend");
|
||||||
|
await tick();
|
||||||
|
gates.splice(0).forEach((gate) => gate.resolve(credential));
|
||||||
|
console.log(JSON.stringify({open, both, canceled: dropped, after: nonce(await after), asked, toasts}));
|
||||||
|
})();
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TestPasskeyPage(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
done = subprocess.run(["node", "-e", PROBE, str(PASSKEY)], capture_output=True, text=True, timeout=60, check=True)
|
||||||
|
cls.data = json.loads(done.stdout)
|
||||||
|
|
||||||
|
def test_a_second_signature_while_one_is_open_asks_no_passkey_and_sends_nothing(self):
|
||||||
|
self.assertEqual(self.data["open"], {"asked": 1, "served": 1, "toasts": ["Resume: not signed"]})
|
||||||
|
self.assertEqual(self.data["both"], [1, None])
|
||||||
|
|
||||||
|
def test_a_canceled_signature_frees_the_next_one(self):
|
||||||
|
self.assertIsNone(self.data["canceled"])
|
||||||
|
self.assertEqual((self.data["after"], self.data["asked"]), (3, 3))
|
||||||
|
self.assertEqual(self.data["toasts"], ["Resume: not signed", "Drain: not signed"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user