mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-15 02:53:25 +10:00
fix(auth): use loopback URL for MCP OAuth JWKS verification (#3297)
* fix(auth): use loopback URL for MCP OAuth JWKS verification Fetch the JWKS endpoint over the internal loopback address instead of the public APP_URL, so token verification works under Docker port-mapping, reverse proxies, and other deployments where the public URL does not loop back to the Node process. Also log the specific MCP OAuth verification error instead of swallowing it with a bare catch. Fixes #3077 * fix(auth): normalize internal JWKS URL and throttle MCP OAuth warnings - Problem: default loopback JWKS URL used PORT in dev where the server listens on SERVER_PORT (3001), and trailing-slash overrides produced //api/auth/jwks; unthrottled warn logs could flood on bad bearer tokens. - Fix: resolveInternalBaseUrl trims/normalizes BETTER_AUTH_INTERNAL_URL, mirrors apps/server listen-port selection, and MCP OAuth warnings are throttled to once per minute. - Verification: pnpm exec biome check on changed files; pnpm typecheck. * fix(auth): declare BETTER_AUTH_INTERNAL_URL in turbo globalEnv - Problem: Turborepo strict env mode strips undeclared BETTER_AUTH_INTERNAL_URL under pnpm dev, so the JWKS override silently falls back to loopback. - Fix: add BETTER_AUTH_INTERNAL_URL to turbo.json globalEnv (required for any new env var per CLAUDE.md). - Verification: python3 JSON parse of turbo.json; confirmed var was absent from globalEnv before this change. --------- Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
co-authored by
Amruth Pillai
parent
5fc9c3ee04
commit
7eb6d3bdbf
@@ -1,5 +1,21 @@
|
||||
import { auth, verifyOAuthToken } from "@reactive-resume/auth/config";
|
||||
|
||||
const OAUTH_WARN_THROTTLE_MS = 60_000;
|
||||
let lastOAuthWarnAt = 0;
|
||||
|
||||
function warnOAuthThrottled(message: string, detail?: unknown): void {
|
||||
const now = Date.now();
|
||||
if (now - lastOAuthWarnAt < OAUTH_WARN_THROTTLE_MS) return;
|
||||
lastOAuthWarnAt = now;
|
||||
|
||||
if (detail !== undefined) {
|
||||
console.warn(message, detail);
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(message);
|
||||
}
|
||||
|
||||
export class AuthError extends Error {
|
||||
constructor() {
|
||||
super("Unauthorized");
|
||||
@@ -13,8 +29,9 @@ export async function authenticateRequest(request: Request): Promise<void> {
|
||||
try {
|
||||
const payload = await verifyOAuthToken(authHeader.slice(7));
|
||||
if (payload?.sub) return;
|
||||
} catch {
|
||||
// Invalid or expired token; fall through to API key auth.
|
||||
warnOAuthThrottled("[MCP] OAuth token verified but missing `sub` claim");
|
||||
} catch (error) {
|
||||
warnOAuthThrottled("[MCP] OAuth token verification failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,23 @@ import { getTrustedOrigins } from "./trusted-origins";
|
||||
const authBaseUrl = env.APP_URL;
|
||||
const isRateLimitEnabled = process.env.NODE_ENV === "production" && !env.FLAG_DISABLE_API_RATE_LIMIT;
|
||||
|
||||
// JWKS must be reachable from inside the Node runtime. `authBaseUrl` is the
|
||||
// publicly-visible URL — under Docker port-mapping or behind a reverse proxy
|
||||
// it does not loop back to the app process. Override with `BETTER_AUTH_INTERNAL_URL`
|
||||
// for split deployments or custom servers that bind to a port not exposed via `PORT`.
|
||||
function resolveInternalBaseUrl(): string {
|
||||
const configured = process.env.BETTER_AUTH_INTERNAL_URL?.trim();
|
||||
if (configured) {
|
||||
return configured.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
const port = process.env.NODE_ENV === "production" ? (process.env.PORT ?? "3000") : String(env.SERVER_PORT);
|
||||
|
||||
return `http://127.0.0.1:${port}`;
|
||||
}
|
||||
|
||||
const internalBaseUrl = resolveInternalBaseUrl();
|
||||
|
||||
const oauthAudienceBase = authBaseUrl.replace(/\/$/, "");
|
||||
const OAUTH_AUDIENCES = [
|
||||
oauthAudienceBase,
|
||||
@@ -38,7 +55,7 @@ const OAUTH_AUDIENCES = [
|
||||
|
||||
export function verifyOAuthToken(token: string): Promise<JWTPayload> {
|
||||
return verifyAccessToken(token, {
|
||||
jwksUrl: `${authBaseUrl}/api/auth/jwks`,
|
||||
jwksUrl: `${internalBaseUrl}/api/auth/jwks`,
|
||||
verifyOptions: {
|
||||
issuer: `${authBaseUrl}/api/auth`,
|
||||
audience: OAUTH_AUDIENCES,
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"DATABASE_URL",
|
||||
"AUTH_SECRET",
|
||||
"BETTER_AUTH_API_KEY",
|
||||
"BETTER_AUTH_INTERNAL_URL",
|
||||
"GOOGLE_CLIENT_ID",
|
||||
"GOOGLE_CLIENT_SECRET",
|
||||
"GITHUB_CLIENT_ID",
|
||||
|
||||
Reference in New Issue
Block a user