diff --git a/plugins/gamecult.heimdall.access/browser-adapter.ts b/plugins/gamecult.heimdall.access/browser-adapter.ts index 82436ac..9ef2a81 100644 --- a/plugins/gamecult.heimdall.access/browser-adapter.ts +++ b/plugins/gamecult.heimdall.access/browser-adapter.ts @@ -116,12 +116,15 @@ export function readHeimdallBrowserReturn( || "Heimdall authentication failed.", }; } + // The attempt id is the handle the app's backend completes over Heimdall's + // private command plane. The completion code in the same fragment is a + // distinct random secret for the HTTP redeem route; this adapter neither + // needs nor forwards it. const attemptId = parameters.get("heimdall_attempt_id") || ""; - const completionCode = parameters.get("heimdall_completion_code") || ""; - if (!attemptId || !completionCode || attemptId !== completionCode) { - return { status: "error", message: "Heimdall returned a malformed authentication completion witness." }; + if (!attemptId) { + return { status: "error", message: "Heimdall returned an authentication completion without its attempt id." }; } - return { status: "success", handle: completionCode }; + return { status: "success", handle: attemptId }; } function clearHeimdallBrowserReturn(): void { diff --git a/tests/browser-adapter.test.ts b/tests/browser-adapter.test.ts index 7ac21b4..d9e1ec6 100644 --- a/tests/browser-adapter.test.ts +++ b/tests/browser-adapter.test.ts @@ -19,7 +19,15 @@ describe("Heimdall browser completion fallback", () => { handle, }); expect(readHeimdallBrowserReturn(returnUrl(handle), "another-app")).toMatchObject({ status: "error" }); - expect(readHeimdallBrowserReturn(returnUrl(handle, "different"), "ghostlight")).toMatchObject({ status: "error" }); + // The completion code is a distinct secret; the handle is the attempt id + // and the code never leaves the fragment. + expect(readHeimdallBrowserReturn(returnUrl(handle, "secret-redeem-code"), "ghostlight")).toEqual({ + status: "success", + handle, + }); + expect(JSON.stringify(readHeimdallBrowserReturn(returnUrl(handle, "secret-redeem-code"), "ghostlight"))) + .not.toContain("secret-redeem-code"); + expect(readHeimdallBrowserReturn(returnUrl("", "secret-redeem-code"), "ghostlight")).toMatchObject({ status: "error" }); expect(readHeimdallBrowserReturn("https://yggdrasil.gamecult.org/ghostlight/", "ghostlight")).toBeUndefined(); }); @@ -72,7 +80,10 @@ describe("Heimdall browser completion fallback", () => { }); }); -function returnUrl(attemptId: string, completionCode = attemptId): string { +// Heimdall issues a random completion code distinct from the attempt id +// (src/app.ts createAuthCompletion); the fixture must too, or it restates the +// contract this adapter used to assume. +function returnUrl(attemptId: string, completionCode = `code-for-${attemptId}`): string { const parameters = new URLSearchParams({ heimdall_status: "success", heimdall_provider: "discord",