fix: restore MCP OAuth registration and authorization (#3421)

* fix: align MCP OAuth provider schema and authorization flow

* test: isolate OpenAPI generation from OAuth initialization

* fix: accept auth routes without a callback query

* fix: require explicit OAuth consent and preserve signed requests

* test: verify OAuth audiences through real MCP initialization

* test(e2e): isolate OAuth token audience validation
This commit is contained in:
Amruth Pillai
2026-09-05 09:33:15 -07:00
committed by GitHub
parent bf71253ca4
commit fe9b59e111
35 changed files with 7837 additions and 144 deletions
+8 -4
View File
@@ -46,6 +46,9 @@ function resolveInternalBaseUrl(): string {
const internalBaseUrl = resolveInternalBaseUrl();
const oauthAudienceBase = authBaseUrl.replace(/\/$/, "");
// These identify the same account-wide API/MCP resource, not separate permission
// tiers. Protected-resource metadata advertises the root; MCP clients may also
// select the mounted endpoint or normalize either URI with a trailing slash.
const OAUTH_AUDIENCES = [
oauthAudienceBase,
`${oauthAudienceBase}/`,
@@ -301,11 +304,12 @@ const getAuthConfig = () => {
}),
oauthProvider({
loginPage: "/api/auth/oauth",
consentPage: "/api/auth/oauth",
validAudiences: OAUTH_AUDIENCES,
consentPage: "/auth/consent",
resources: OAUTH_AUDIENCES,
clientRegistrationDefaultResources: OAUTH_AUDIENCES,
allowDynamicClientRegistration: true,
// Required for MCP client onboarding (RFC 7591). Phishing vector is closed by the
// redirect_uri policy in the hooks.before middleware above and server auth preflight.
// Required for MCP client onboarding (RFC 7591). Redirect URI validation
// and explicit user consent protect access by dynamically registered clients.
allowUnauthenticatedClientRegistration: true,
rateLimit: oauthProviderRateLimit,
silenceWarnings: { oauthAuthServerConfig: true },
+18
View File
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { oauthProvider } from "@better-auth/oauth-provider";
import { getTableColumns, is, Table } from "drizzle-orm";
import * as dbSchema from "@reactive-resume/db/schema";
const plugin = oauthProvider({ loginPage: "/login", consentPage: "/consent" });
describe("OAuth provider persistence schema", () => {
it.each(Object.entries(plugin.schema))("provides every installed plugin field for %s", (modelName, model) => {
const table = Reflect.get(dbSchema, modelName);
expect(is(table, Table), `Missing table ${modelName}`).toBe(true);
if (!is(table, Table)) return;
const columns = getTableColumns(table);
for (const field of Object.keys(model.fields)) {
expect(columns, `${modelName}.${field}`).toHaveProperty(field);
}
});
});