49 lines
1.9 KiB
TypeScript
49 lines
1.9 KiB
TypeScript
import { expect, test, spyOn } from "bun:test";
|
|
import { readFileSync } from "node:fs";
|
|
import { Window } from "happy-dom";
|
|
|
|
test("passkey sign-in follows the server handoff, including cross-app destinations", async () => {
|
|
for (const redirect of [
|
|
"https://account.example.test/?sso_code=synthetic-code",
|
|
"/signed-in",
|
|
"javascript:alert(1)",
|
|
]) {
|
|
const window = new Window({ url: "https://sso.example.test/login" });
|
|
const assign = spyOn(window.location, "assign").mockImplementation(() => {});
|
|
try {
|
|
window.document.body.innerHTML = `<div data-auth-passkey><button>Sign in</button><p data-auth-status></p></div>`;
|
|
Object.defineProperty(window, "PublicKeyCredential", { value: class {} });
|
|
Object.defineProperty(window.navigator, "credentials", {
|
|
value: {
|
|
get: async () => ({
|
|
id: "synthetic-credential",
|
|
type: "public-key",
|
|
rawId: new Uint8Array([1]),
|
|
response: {
|
|
clientDataJSON: new Uint8Array([1]),
|
|
authenticatorData: new Uint8Array([2]),
|
|
signature: new Uint8Array([3]),
|
|
},
|
|
}),
|
|
},
|
|
});
|
|
window.fetch = (async (url: string) =>
|
|
Response.json(
|
|
url.endsWith("/options")
|
|
? { ok: true, key: "test-key", options: { challenge: "AQID" } }
|
|
: { ok: true, redirect },
|
|
)) as any;
|
|
window.eval(readFileSync(new URL("../assets/client/auth.js", import.meta.url), "utf8"));
|
|
await (window as any).WRNexusAuth.authenticatePasskey(window.document.querySelector("div"));
|
|
if (redirect.startsWith("javascript:")) {
|
|
expect(assign).not.toHaveBeenCalled();
|
|
} else {
|
|
expect(assign).toHaveBeenCalledWith(new URL(redirect, window.location.href).href);
|
|
}
|
|
} finally {
|
|
assign.mockRestore();
|
|
await window.happyDOM.close();
|
|
}
|
|
}
|
|
});
|