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:
parent
47747ebc99
commit
e8e2832230
2 changed files with 20 additions and 6 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue