Start: a profile with startRequiresAppCaller refuses unauthenticated starts; set on Bifrost

Bifrost's browser binding is the secrecy of a nonce carried readable in the signed state, so only Bifrost may mint a Bifrost state. The gate sits in startOAuthFlow beside the entitlementPolicy caller check, so HTTP and the private command plane share it. Follows the red tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-22 18:14:37 +02:00
commit 7e17a0e952
2 changed files with 13 additions and 1 deletions

View file

@ -45,6 +45,17 @@ export interface AppProfile {
* plane; absent for apps with no such requirement.
*/
requiredEntitlementPolicyKind?: OAuthEntitlementPolicy["kind"];
/**
* The app binds browser completion to its own pending attempt, and the
* binding is the secrecy of the attempt nonce, which rides readable in the
* signed (not encrypted) state's return_to. If anyone could start a flow for
* this app, whoever learned a victim's nonce could mint a completion bound
* to it and sign the victim's browser in as themselves (login CSRF, finding
* known-nonce-login-csrf). So a start for this app is refused unless the
* caller is this app's authenticated caller: only the app can mint its own
* state. Absent for apps whose browser starts are public by design.
*/
startRequiresAppCaller?: boolean;
}
/** The membership signal shared by every app that gates on GameCult membership. */
@ -142,6 +153,7 @@ const bifrostProfile: AppProfile = {
entitlementSources: ["discord", "patreon"],
managedConnectionProviders: [],
allowedReturnOrigins: ["https://bifrost.gamecult.org"],
startRequiresAppCaller: true,
capabilities: [
{
key: "member_access",

View file

@ -554,7 +554,7 @@ export async function startOAuthFlow(
if (options?.trustedBrowserAttemptId && handoff.kind === "browser_completion") {
handoff = { kind: "browser_completion", attemptId: options.trustedBrowserAttemptId };
}
if (entitlementPolicy && (!isAppCaller(caller) || caller.appSlug !== profile.slug)) {
if ((entitlementPolicy || profile.startRequiresAppCaller) && (!isAppCaller(caller) || caller.appSlug !== profile.slug)) {
return { statusCode: 401, body: { error: "app_auth_required" } };
}