Skip to content

Commit

Permalink
fix(backend,nextjs,remix,clerk-sdk-node): Allow for a preferred host …
Browse files Browse the repository at this point in the history
…source
  • Loading branch information
tmilewski committed Nov 17, 2023
1 parent deac67c commit 8356d44
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 27 deletions.
8 changes: 8 additions & 0 deletions .changeset/sour-squids-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@clerk/clerk-sdk-node': patch
'@clerk/backend': patch
'@clerk/nextjs': patch
'@clerk/remix': patch
---

Allow for a preferred source for the host attribute: default(host) | x-forwarded-host
5 changes: 4 additions & 1 deletion packages/backend/src/util/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ export function checkCrossOrigin({
forwardedHost?: string | null;
forwardedProto?: string | null;
}) {
const finalURL = buildOrigin({ forwardedProto, forwardedHost, protocol: originURL.protocol, host });
const finalURL = buildOrigin(
{ forwardedProto, forwardedHost, protocol: originURL.protocol, host },
{ hostPreference: 'forwarded' },
);
return finalURL && new URL(finalURL).origin !== originURL.origin;
}

Expand Down
153 changes: 136 additions & 17 deletions packages/backend/src/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type QUnit from 'qunit';

import { buildOrigin, buildRequestUrl } from './utils';
import { buildOrigin, type BuildOriginOptions, buildRequestUrl } from './utils';

const fwdOpts: BuildOriginOptions = { hostPreference: 'forwarded' };

export default (QUnit: QUnit) => {
const { module, test } = QUnit;

module('buildOrigin({ protocol, forwardedProto, forwardedHost, host })', () => {
module('buildOrigin({ protocol, forwardedProto, forwardedHost, host }, { hostPreference: "default" })', () => {
test('without any param', assert => {
assert.equal(buildOrigin({}), '');
});
Expand All @@ -24,48 +26,48 @@ export default (QUnit: QUnit) => {

test('with forwarded proto', assert => {
assert.equal(
buildOrigin({ protocol: 'http', host: 'localhost:3000', forwardedProto: 'https' }),
'https://localhost:3000',
buildOrigin({ protocol: 'http', host: 'example-host.com', forwardedProto: 'https' }),
'https://example-host.com',
);
});

test('with forwarded proto - with multiple values', assert => {
assert.equal(
buildOrigin({ protocol: 'http', host: 'localhost:3000', forwardedProto: 'https,http' }),
'https://localhost:3000',
buildOrigin({ protocol: 'http', host: 'example-host.com', forwardedProto: 'https,http' }),
'https://example-host.com',
);
});

test('with forwarded host', assert => {
assert.equal(
buildOrigin({ protocol: 'http', host: 'localhost:3000', forwardedHost: 'example.com' }),
'http://example.com',
buildOrigin({ protocol: 'http', host: 'example-host.com', forwardedHost: 'example.com' }),
'http://example-host.com',
);
});

test('with forwarded host - with multiple values', assert => {
assert.equal(
buildOrigin({ protocol: 'http', host: 'localhost:3000', forwardedHost: 'example.com,example-2.com' }),
'http://example.com',
buildOrigin({ protocol: 'http', host: 'example-host.com', forwardedHost: 'example.com,example-2.com' }),
'http://example-host.com',
);
});

test('with forwarded proto and host', assert => {
assert.equal(
buildOrigin({
protocol: 'http',
host: 'localhost:3000',
host: 'example-host.com',
forwardedProto: 'https',
forwardedHost: 'example.com',
}),
'https://example.com',
'https://example-host.com',
);
});

test('with forwarded proto and host - without protocol', assert => {
assert.equal(
buildOrigin({ host: 'localhost:3000', forwardedProto: 'https', forwardedHost: 'example.com' }),
'https://example.com',
buildOrigin({ host: 'example-host.com', forwardedProto: 'https', forwardedHost: 'example.com' }),
'https://example-host.com',
);
});

Expand All @@ -81,6 +83,88 @@ export default (QUnit: QUnit) => {
});
});

module('buildOrigin({ protocol, forwardedProto, forwardedHost, host }, { hostPreference: "forwarded" })', () => {
test('without any param', assert => {
assert.equal(buildOrigin({}, fwdOpts), '');
});

test('with protocol', assert => {
assert.equal(buildOrigin({ protocol: 'http' }, fwdOpts), '');
});

test('with host', assert => {
assert.equal(buildOrigin({ host: 'localhost:3000' }, fwdOpts), '');
});

test('with protocol and host', assert => {
assert.equal(buildOrigin({ protocol: 'http', host: 'localhost:3000' }, fwdOpts), 'http://localhost:3000');
});

test('with forwarded proto', assert => {
assert.equal(
buildOrigin({ protocol: 'http', host: 'localhost:3000', forwardedProto: 'https' }, fwdOpts),
'https://localhost:3000',
);
});

test('with forwarded proto - with multiple values', assert => {
assert.equal(
buildOrigin({ protocol: 'http', host: 'localhost:3000', forwardedProto: 'https,http' }, fwdOpts),
'https://localhost:3000',
);
});

test('with forwarded host', assert => {
assert.equal(
buildOrigin({ protocol: 'http', host: 'localhost:3000', forwardedHost: 'example.com' }, fwdOpts),
'http://example.com',
);
});

test('with forwarded host - with multiple values', assert => {
assert.equal(
buildOrigin({ protocol: 'http', host: 'localhost:3000', forwardedHost: 'example.com,example-2.com' }, fwdOpts),
'http://example.com',
);
});

test('with forwarded proto and host', assert => {
assert.equal(
buildOrigin(
{
protocol: 'http',
host: 'localhost:3000',
forwardedProto: 'https',
forwardedHost: 'example.com',
},
fwdOpts,
),
'https://example.com',
);
});

test('with forwarded proto and host - without protocol', assert => {
assert.equal(
buildOrigin({ host: 'localhost:3000', forwardedProto: 'https', forwardedHost: 'example.com' }, fwdOpts),
'https://example.com',
);
});

test('with forwarded proto and host - without host', assert => {
assert.equal(
buildOrigin({ protocol: 'http', forwardedProto: 'https', forwardedHost: 'example.com' }, fwdOpts),
'https://example.com',
);
});

test('with forwarded proto and host - without host and protocol', assert => {
assert.equal(
buildOrigin({ forwardedProto: 'https', forwardedHost: 'example.com' }, fwdOpts),
'https://example.com',
);
});
});

module('buildRequestUrl({ request, path })', () => {
test('without headers', assert => {
const req = new Request('http://localhost:3000/path');
Expand All @@ -89,9 +173,9 @@ export default (QUnit: QUnit) => {

test('with forwarded proto / host headers', assert => {
const req = new Request('http://localhost:3000/path', {
headers: { 'x-forwarded-host': 'example.com', 'x-forwarded-proto': 'https,http' },
headers: { 'x-forwarded-host': 'example.com', 'x-forwarded-proto': 'https,http', host: 'example-host.com' },
});
assert.equal(buildRequestUrl(req), 'https://example.com/path');
assert.equal(buildRequestUrl(req), 'https://example-host.com/path');
});

test('with forwarded proto / host and host headers', assert => {
Expand All @@ -102,7 +186,7 @@ export default (QUnit: QUnit) => {
host: 'example-host.com',
},
});
assert.equal(buildRequestUrl(req), 'https://example.com/path');
assert.equal(buildRequestUrl(req), 'https://example-host.com/path');
});

test('with path', assert => {
Expand All @@ -115,4 +199,39 @@ export default (QUnit: QUnit) => {
assert.equal(buildRequestUrl(req), 'http://localhost:3000/path');
});
});

module('buildRequestUrl({ request, path }, { hostPreference: "forwarded" })', () => {
test('without headers', assert => {
const req = new Request('http://localhost:3000/path');
assert.equal(buildRequestUrl(req, fwdOpts), 'http://localhost:3000/path');
});

test('with forwarded proto / host headers', assert => {
const req = new Request('http://localhost:3000/path', {
headers: { 'x-forwarded-host': 'example.com', 'x-forwarded-proto': 'https,http' },
});
assert.equal(buildRequestUrl(req, fwdOpts), 'https://example.com/path');
});

test('with forwarded proto / host and host headers', assert => {
const req = new Request('http://localhost:3000/path', {
headers: {
'x-forwarded-host': 'example.com',
'x-forwarded-proto': 'https,http',
host: 'example-host.com',
},
});
assert.equal(buildRequestUrl(req, fwdOpts), 'https://example.com/path');
});

test('with path', assert => {
const req = new Request('http://localhost:3000/path');
assert.equal(buildRequestUrl(req, '/other-path', fwdOpts), 'http://localhost:3000/other-path');
});

test('with query params in request', assert => {
const req = new Request('http://localhost:3000/path');
assert.equal(buildRequestUrl(req, fwdOpts), 'http://localhost:3000/path');
});
});
};
35 changes: 29 additions & 6 deletions packages/backend/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,52 @@ import { constants } from './constants';
const getHeader = (req: Request, key: string) => req.headers.get(key);
const getFirstValueFromHeader = (value?: string | null) => value?.split(',')[0];

type BuildRequestUrl = (request: Request, path?: string) => URL;
export const buildRequestUrl: BuildRequestUrl = (request, path) => {
type BuildRequestUrl =
| ((request: Request, path?: string | BuildOriginOptions) => URL)
| ((request: Request, opts?: string | BuildOriginOptions) => URL)
| ((request: Request, pathOrOpts?: string | BuildOriginOptions, opts?: BuildOriginOptions) => URL);

export const buildRequestUrl: BuildRequestUrl = (request, pathOrOpts, opts) => {
const initialUrl = new URL(request.url);

let path: string | undefined;
let options: BuildOriginOptions | undefined;

if (typeof pathOrOpts === 'object' && 'hostPreference' in pathOrOpts) {
options = pathOrOpts;
} else {
path = pathOrOpts;
options = opts;
}

const forwardedProto = getHeader(request, constants.Headers.ForwardedProto);
const forwardedHost = getHeader(request, constants.Headers.ForwardedHost);
const host = getHeader(request, constants.Headers.Host);
const protocol = initialUrl.protocol;

const base = buildOrigin({ protocol, forwardedProto, forwardedHost, host: host || initialUrl.host });
const base = buildOrigin({ protocol, forwardedProto, forwardedHost, host: host || initialUrl.host }, options);

return new URL(path || initialUrl.pathname, base);
};

export type BuildOriginOptions = {
hostPreference: 'default' | 'forwarded';
};

type BuildOriginParams = {
protocol?: string;
forwardedProto?: string | null;
forwardedHost?: string | null;
host?: string | null;
};
type BuildOrigin = (params: BuildOriginParams) => string;
export const buildOrigin: BuildOrigin = ({ protocol, forwardedProto, forwardedHost, host }) => {
const resolvedHost = getFirstValueFromHeader(forwardedHost) ?? host;
type BuildOrigin = (params: BuildOriginParams, options?: BuildOriginOptions) => string;

export const buildOrigin: BuildOrigin = ({ protocol, forwardedProto, forwardedHost, host }, opts) => {
const { hostPreference = 'default' } = opts || {};

const fwd = getFirstValueFromHeader(forwardedHost);

const resolvedHost = hostPreference === 'default' ? host ?? fwd : fwd ?? host;
const resolvedProtocol = getFirstValueFromHeader(forwardedProto) ?? protocol?.replace(/[:/]/, '');

if (!resolvedHost || !resolvedProtocol) {
Expand Down
1 change: 0 additions & 1 deletion packages/nextjs/src/server/authMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ const authMiddleware: AuthMiddleware = (...args: unknown[]) => {
const req = withNormalizedClerkUrl(_req);

logger.debug('URL debug', {
url: req.nextUrl.href,
method: req.method,
headers: stringifyHeaders(req.headers),
nextUrl: req.nextUrl.href,
Expand Down
2 changes: 1 addition & 1 deletion packages/remix/src/ssr/authenticateRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function authenticateRequest(args: LoaderFunctionArgs, opts: RootAuthLoad
isTruthy(getEnvVariable('CLERK_IS_SATELLITE', context)) ||
false;

const requestURL = buildRequestUrl(request);
const requestURL = buildRequestUrl(request, { hostPreference: 'forwarded' });

const relativeOrAbsoluteProxyUrl = handleValueOrFn(
opts?.proxyUrl,
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-node/src/authenticateRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const authenticateRequest = (opts: AuthenticateRequestParams) => {
});
});

const requestUrl = buildRequestUrl(isomorphicRequest);
const requestUrl = buildRequestUrl(isomorphicRequest, { hostPreference: 'forwarded' });
const isSatellite = handleValueOrFn(options?.isSatellite, requestUrl, env.isSatellite);
const domain = handleValueOrFn(options?.domain, requestUrl) || env.domain;
const signInUrl = options?.signInUrl || env.signInUrl;
Expand Down

0 comments on commit 8356d44

Please sign in to comment.