Delete the global GC_ACCESS_APP_SHARED_SECRET fallback (R6, fork 2)
Per-app identity with a global fallback made repixelizer and streampixels one identity in practice. Each app now needs its own GC_ACCESS_APP_<SLUG>_SHARED_SECRET; operator mints distinct values on Yggdrasil in the same deploy window. Doc note in service-contract.md corrected to stop advertising the fallback.
This commit is contained in:
parent
c6d0ecb68b
commit
326f613ffe
3 changed files with 30 additions and 4 deletions
|
|
@ -439,8 +439,8 @@ Purpose:
|
|||
Authentication:
|
||||
|
||||
- requires `x-heimdall-app-secret`
|
||||
- the configured secret comes from `GC_ACCESS_APP_{APP_SLUG}_SHARED_SECRET` or
|
||||
the generic `GC_ACCESS_APP_SHARED_SECRET`
|
||||
- the configured secret comes from `GC_ACCESS_APP_{APP_SLUG}_SHARED_SECRET`,
|
||||
per app; there is no generic fallback secret shared across apps
|
||||
|
||||
Request body:
|
||||
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ export function loadConfig(
|
|||
appSlugs
|
||||
.map((appSlug) => {
|
||||
const envKey = `GC_ACCESS_APP_${appSlug.toUpperCase()}_SHARED_SECRET`;
|
||||
return [appSlug, env[envKey] ?? env.GC_ACCESS_APP_SHARED_SECRET];
|
||||
return [appSlug, env[envKey]];
|
||||
})
|
||||
.filter(([, value]) => Boolean(value))
|
||||
) as Partial<Record<AppSlug, string>>;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { type FastifyInstance } from "fastify";
|
||||
import { buildApp } from "../src/app.js";
|
||||
import { type HeimdallConfig } from "../src/config.js";
|
||||
import { type HeimdallConfig, loadConfig } from "../src/config.js";
|
||||
import { entitlementFacts, identityFacts } from "../src/facts.js";
|
||||
import { createOAuthRuntimeRegistry, type OAuthProviderRuntime } from "../src/oauth.js";
|
||||
import { verifyJwt } from "../src/signing.js";
|
||||
|
|
@ -1091,6 +1091,32 @@ describe("Heimdall service", () => {
|
|||
expect(credentialResponse.body).not.toContain("rotated-twitch-refresh-token");
|
||||
});
|
||||
|
||||
it("no longer honors a global shared secret across apps", async () => {
|
||||
// A per-app identity model with a global fallback is a two-app shared
|
||||
// secret in practice (R6, fork 2). GC_ACCESS_APP_SHARED_SECRET alone must
|
||||
// produce no usable secret for any app.
|
||||
const config = loadConfig({ GC_ACCESS_APP_SHARED_SECRET: "one-secret-for-everyone" });
|
||||
expect(config.appSharedSecrets).toEqual({});
|
||||
|
||||
const app = await buildApp({
|
||||
config: {
|
||||
...createTestConfig(),
|
||||
appSharedSecrets: {},
|
||||
},
|
||||
});
|
||||
apps.push(app);
|
||||
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: "/v1/apps/streampixels/managed-credentials/resolve",
|
||||
headers: { "x-heimdall-app-secret": "one-secret-for-everyone" },
|
||||
payload: { accountId: "acct-1", provider: "twitch" },
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(401);
|
||||
expect(response.json()).toEqual(expect.objectContaining({ error: "app_auth_required" }));
|
||||
});
|
||||
|
||||
it("accepts Twitch token scopes returned as an array", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
new Response(
|
||||
|
|
|
|||
Loading…
Reference in a new issue