Resume browser sign-in by attempt id, not the completion code

The Discord callback has issued a random completion code distinct from the
attempt id since the redemption-code fix, but the browser adapter still
required heimdall_attempt_id == heimdall_completion_code and returned
"malformed authentication completion witness" for every real return, so no
app that vendors the adapter could finish a browser sign-in. The test
fixture defaulted the code to the attempt id, restating the old contract.

The adapter now completes with the attempt id, the handle the app backend
redeems over the private command plane, and never reads the completion
code. The fixture issues a distinct code as the server does; returning the
code as the handle fails the tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-23 20:50:28 +02:00
commit e8e2832230
2 changed files with 20 additions and 6 deletions

View file

@ -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 {

View file

@ -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",