mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-03 10:13:47 +10:00
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:
@@ -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 },
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -250,6 +250,16 @@ export const jwks = pg.pgTable("jwks", {
|
||||
export const oauthClient = pg.pgTable(
|
||||
"oauth_client",
|
||||
{
|
||||
// Additive fields required by Better Auth 1.7 OAuth provider.
|
||||
clientDiscoveryId: pg.text("client_discovery_id"),
|
||||
clientCredentialsScopes: pg.text("client_credentials_scopes").array().default([]),
|
||||
backchannelLogoutUri: pg.text("backchannel_logout_uri"),
|
||||
backchannelLogoutSessionRequired: pg.boolean("backchannel_logout_session_required"),
|
||||
applicationType: pg.text("application_type"),
|
||||
jwks: pg.text("jwks"),
|
||||
jwksUri: pg.text("jwks_uri"),
|
||||
dpopBoundAccessTokens: pg.boolean("dpop_bound_access_tokens").default(false),
|
||||
|
||||
id: pg
|
||||
.text("id")
|
||||
.notNull()
|
||||
@@ -294,6 +304,15 @@ export const oauthClient = pg.pgTable(
|
||||
export const oauthRefreshToken = pg.pgTable(
|
||||
"oauth_refresh_token",
|
||||
{
|
||||
// Additive fields required by Better Auth 1.7 OAuth provider.
|
||||
authorizationCodeId: pg.text("authorization_code_id"),
|
||||
resources: pg.text("resources").array(),
|
||||
requestedUserInfoClaims: pg.text("requested_user_info_claims").array(),
|
||||
rotatedAt: pg.timestamp("rotated_at", { withTimezone: true }),
|
||||
rotationReplayResponse: pg.text("rotation_replay_response"),
|
||||
rotationReplayExpiresAt: pg.timestamp("rotation_replay_expires_at", { withTimezone: true }),
|
||||
confirmation: pg.jsonb("confirmation"),
|
||||
|
||||
id: pg
|
||||
.text("id")
|
||||
.notNull()
|
||||
@@ -322,6 +341,13 @@ export const oauthRefreshToken = pg.pgTable(
|
||||
export const oauthAccessToken = pg.pgTable(
|
||||
"oauth_access_token",
|
||||
{
|
||||
// Additive fields required by Better Auth 1.7 OAuth provider.
|
||||
authorizationCodeId: pg.text("authorization_code_id"),
|
||||
resources: pg.text("resources").array(),
|
||||
requestedUserInfoClaims: pg.text("requested_user_info_claims").array(),
|
||||
revoked: pg.timestamp("revoked", { withTimezone: true }),
|
||||
confirmation: pg.jsonb("confirmation"),
|
||||
|
||||
id: pg
|
||||
.text("id")
|
||||
.notNull()
|
||||
@@ -346,6 +372,10 @@ export const oauthAccessToken = pg.pgTable(
|
||||
export const oauthConsent = pg.pgTable(
|
||||
"oauth_consent",
|
||||
{
|
||||
// Additive fields required by Better Auth 1.7 OAuth provider.
|
||||
resources: pg.text("resources").array(),
|
||||
requestedUserInfoClaims: pg.text("requested_user_info_claims").array(),
|
||||
|
||||
id: pg
|
||||
.text("id")
|
||||
.notNull()
|
||||
@@ -366,3 +396,56 @@ export const oauthConsent = pg.pgTable(
|
||||
},
|
||||
(t) => [pg.index().on(t.userId, t.clientId)],
|
||||
);
|
||||
|
||||
export const oauthResource = pg.pgTable("oauth_resource", {
|
||||
id: pg
|
||||
.text("id")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => generateId()),
|
||||
identifier: pg.text("identifier").notNull().unique(),
|
||||
name: pg.text("name").notNull(),
|
||||
accessTokenTtl: pg.integer("access_token_ttl"),
|
||||
refreshTokenTtl: pg.integer("refresh_token_ttl"),
|
||||
signingAlgorithm: pg.text("signing_algorithm"),
|
||||
signingKeyId: pg.text("signing_key_id"),
|
||||
allowedScopes: pg.text("allowed_scopes").array(),
|
||||
customClaims: pg.jsonb("custom_claims"),
|
||||
dpopBoundAccessTokensRequired: pg.boolean("dpop_bound_access_tokens_required").default(false),
|
||||
disabled: pg.boolean("disabled").default(false),
|
||||
createdAt: pg.timestamp("created_at", { withTimezone: true }),
|
||||
updatedAt: pg.timestamp("updated_at", { withTimezone: true }),
|
||||
policyVersion: pg.integer("policy_version").default(1),
|
||||
metadata: pg.jsonb("metadata"),
|
||||
});
|
||||
|
||||
export const oauthClientResource = pg.pgTable(
|
||||
"oauth_client_resource",
|
||||
{
|
||||
id: pg
|
||||
.text("id")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => generateId()),
|
||||
clientId: pg
|
||||
.text("client_id")
|
||||
.notNull()
|
||||
.references(() => oauthClient.clientId, { onDelete: "cascade" }),
|
||||
resourceId: pg
|
||||
.text("resource_id")
|
||||
.notNull()
|
||||
.references(() => oauthResource.identifier, { onDelete: "cascade" }),
|
||||
metadata: pg.jsonb("metadata"),
|
||||
createdAt: pg.timestamp("created_at", { withTimezone: true }),
|
||||
},
|
||||
(t) => [pg.index().on(t.clientId), pg.index().on(t.resourceId), pg.uniqueIndex().on(t.clientId, t.resourceId)],
|
||||
);
|
||||
|
||||
export const oauthClientAssertion = pg.pgTable("oauth_client_assertion", {
|
||||
id: pg
|
||||
.text("id")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => generateId()),
|
||||
expiresAt: pg.timestamp("expires_at", { withTimezone: true }).notNull(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user