From 32208cc8e2b2e6cf946448b1af6d46212494eed6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 19 Oct 2026 13:27:28 +0200 Subject: [PATCH] Pin the callback's refusal of a signed state the provider has no role for Re-signs a joystick connect state as sign_in and link with the app's own key and shows the callback refuses before the code exchange and writes nothing. Co-Authored-By: Claude Opus 5.5 --- tests/app.test.ts | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/app.test.ts b/tests/app.test.ts index b086853..3afe0a6 100644 --- a/tests/app.test.ts +++ b/tests/app.test.ts @@ -1,3 +1,6 @@ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { inspect } from "node:util"; import { afterEach, describe, expect, it } from "vitest"; import { type FastifyInstance } from "fastify"; @@ -7,7 +10,7 @@ import { deliverBackendHandoff, type BackendHandoffPayload } from "../src/backen import { type HeimdallConfig, loadConfig } from "../src/config.js"; import { entitlementFacts, identityFacts } from "../src/facts.js"; import { createOAuthRuntimeRegistry, ProviderResponseError, type OAuthProviderRuntime } from "../src/oauth.js"; -import { verifyJwt } from "../src/signing.js"; +import { createRuntimeKeyMaterial, signJwt, verifyJwt } from "../src/signing.js"; import { InMemoryStore } from "../src/store/index.js"; function createTestConfig(): HeimdallConfig { @@ -1420,6 +1423,37 @@ describe("Heimdall service", () => { return calls; } + it.each(["sign_in", "link"] as const)( + "the callback refuses a signed %s state for joystick on streampixels before the code exchange, and nothing is written", + async (mode) => { + // The start gate already refuses these words, so the state is re-signed with the app's own key. + const dir = mkdtempSync(join(tmpdir(), "heimdall-forge-")); + try { + const config = { ...createTestConfig(), signingPrivateKeyPath: join(dir, "signing.pem"), bootstrapSigningPrivateKeyOnMissing: true }; + const store = new InMemoryStore(); + const app = await buildApp({ config, store }); + apps.push(app); + const keys = createRuntimeKeyMaterial({ ...config, bootstrapSigningPrivateKeyOnMissing: false }); + const calls = stubJoystick({ channel_id: "chan-forged", username: "deru" }); + const stateToken = (await connect(app)).json().stateToken as string; + const verified = verifyJwt(stateToken, keys.publicKey); + expect(verified.valid).toBe(true); + const forged = signJwt({ ...(verified as { payload: Record }).payload, mode }, keys); + + const callback = await app.inject({ + method: "GET", + url: `/v1/oauth/joystick/callback?code=c&state=${encodeURIComponent(forged)}`, + }); + expect(callback.statusCode).toBeGreaterThanOrEqual(400); + expect(callback.body).not.toContain("viewer_access"); + expect(calls).toEqual([]); + expect(await store.findStoredLinkedIdentity("joystick", "chan-forged")).toBeNull(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + } + ); + it("exchanges the code with Basic client auth and links the identity by channel id", async () => { const store = new InMemoryStore(); const app = await buildApp({ config: createTestConfig(), store });