fix(auth): strip internal port from OAuth redirects on BYOD domains (#728)

* fix(auth): strip internal port from OAuth redirects on BYOD domains

When BYOD custom domains proxy auth requests through the dashboard to
PRIVATE_SERVER_URL (e.g. http://cio-api-vymh:10000), Node's URL host setter
preserves the upstream port. Better Auth then redirects browsers to
https://customer.example:10000/... which fails.

- Add oauth-proxy-redirect helpers to clear backend ports explicitly
- Rewrite inbound auth request URLs and outbound oauth-proxy-callback Location headers
- Send hostname-only X-Forwarded-Host from dashboard proxy when port matches backend

Co-authored-by: Rotimi Best <[email protected]>

* fix(auth): address Greptile review on oauth port stripping

- Only treat PRIVATE_SERVER_URL as an internal backend port
- Add test for rewriteAuthRequestUrl when forwarded host still has :10000

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Rotimi Best
2026-07-11 06:17:26 +01:00
committed by GitHub
co-authored by Rotimi Best Cursor Agent
parent 3eb5e1268d
commit 2fb6b1c477
4 changed files with 255 additions and 14 deletions
+29 -11
View File
@@ -6,6 +6,12 @@ import { isPublicCorsPath, sessionCors } from '@api/middlewares/cors';
import { randomBytes } from 'crypto';
import { API_SERVER_URL } from '@api/constants';
import {
resolveOAuthBrowserOrigin,
rewriteAuthRequestUrl,
rewriteOAuthProxyCallbackLocation,
shouldRewriteOAuthProxyCallbackLocation
} from '@api/utils/oauth-proxy-redirect';
import { Hono } from '@api/utils/hono';
import { ErrorCodes } from '@api/utils/errors';
import { accountRouter } from '@api/routes/account';
@@ -114,13 +120,16 @@ export const app = new Hono()
let request = c.req.raw;
const fwdHost = c.req.header('x-forwarded-host');
const fwdProto = c.req.header('x-forwarded-proto');
const browserOrigin = resolveOAuthBrowserOrigin({
forwardedHost: fwdHost,
forwardedProto: fwdProto
});
if (fwdHost) {
const url = new URL(request.url);
url.host = fwdHost;
if (fwdProto === 'https' || fwdProto === 'http') {
url.protocol = `${fwdProto}:`;
}
request = new Request(url, request);
request = rewriteAuthRequestUrl(request, {
forwardedHost: fwdHost,
forwardedProto: fwdProto
});
}
// Inbound: if the browser is hitting `/oauth-proxy-callback?token=…`
@@ -168,8 +177,9 @@ export const app = new Hono()
// pushes the Location header past Render's response size ceiling and
// the redirect silently never reaches the browser.
if (response.status >= 300 && response.status < 400) {
const location = response.headers.get('location');
if (location && location.includes('/oauth-proxy-callback') && location.includes('cookies=')) {
let location = response.headers.get('location');
if (location?.includes('/oauth-proxy-callback') && location.includes('cookies=')) {
try {
const outUrl = new URL(location);
const cookies = outUrl.searchParams.get('cookies');
@@ -178,14 +188,22 @@ export const app = new Hono()
await storeHandoffPayload(token, cookies);
outUrl.searchParams.delete('cookies');
outUrl.searchParams.set('token', token);
const headers = new Headers(response.headers);
headers.set('location', outUrl.toString());
return new Response(response.body, { status: response.status, headers });
location = outUrl.toString();
}
} catch (error) {
console.error('[auth-handler] failed to swap cookies → token, falling back to inline cookies:', error);
}
}
if (location && browserOrigin && shouldRewriteOAuthProxyCallbackLocation(location, browserOrigin)) {
location = rewriteOAuthProxyCallbackLocation(location, browserOrigin);
}
if (location && location !== response.headers.get('location')) {
const headers = new Headers(response.headers);
headers.set('location', location);
return new Response(response.body, { status: response.status, headers });
}
}
if (response.status >= 300 && response.status < 400) {