Skip to content

Commit

Permalink
feat(backend): Expose callback with Request and Headers as a param of…
Browse files Browse the repository at this point in the history
… createIsomorphicRequest

chore(backend): Add changeset for `createIsomorphicRequest`

refactor(clerk-sdk-node,backend): Remove sdk-node cookie dependency

Also refactor authenticateRequest options

fix(backend): Refactor the new IsomorphicRequest utilities

Also, manipulate headers objects to be compatible with Headers constructor

chore(repo): Revert `package-lock.json` changes
  • Loading branch information
anagstef committed Jul 7, 2023
1 parent b9e2324 commit 04045b1
Show file tree
Hide file tree
Showing 18 changed files with 183 additions and 106 deletions.
12 changes: 12 additions & 0 deletions .changeset/funny-melons-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'gatsby-plugin-clerk': minor
'@clerk/clerk-sdk-node': minor
'@clerk/backend': minor
'@clerk/fastify': minor
'@clerk/nextjs': minor
'@clerk/remix': minor
---

Introduce `createIsomorphicRequest` in `@clerk/backend`

This utility simplifies the `authenticateRequest` signature, and it makes it easier to integrate with more frameworks.
1 change: 1 addition & 0 deletions packages/backend/src/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default (QUnit: QUnit) => {
'Verification',
'constants',
'createAuthenticateRequest',
'createIsomorphicRequest',
'debugRequestState',
'decodeJwt',
'deserialize',
Expand Down
11 changes: 0 additions & 11 deletions packages/backend/src/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,6 @@ const {
RuntimeResponse,
} = fetchApisPolyfill;

// @ts-ignore
console.log(__BUILD__, {
fetch,
RuntimeAbortController,
RuntimeBlob,
RuntimeFormData,
RuntimeHeaders,
RuntimeRequest,
RuntimeResponse,
});

type Runtime = {
crypto: Crypto;
fetch: typeof global.fetch;
Expand Down
40 changes: 13 additions & 27 deletions packages/backend/src/tokens/request.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { API_URL, API_VERSION, constants } from '../constants';
import { assertValidSecretKey } from '../util/assertValidSecretKey';
import { isDevelopmentFromApiKey } from '../util/instance';
import { parseIsomorphicRequestCookies } from '../util/IsomorphicRequest';
import { buildRequest, stripAuthorizationHeader } from '../util/IsomorphicRequest';
import { parsePublishableKey } from '../util/parsePublishableKey';
import type { RequestState } from './authStatus';
import { AuthErrorReason, interstitial, signedOut, unknownState } from './authStatus';
Expand Down Expand Up @@ -111,34 +111,24 @@ function assertProxyUrlOrDomain(proxyUrlOrDomain: string | undefined) {
}

export async function authenticateRequest(options: AuthenticateRequestOptions): Promise<RequestState> {
const { request: isomorphicRequest } = options;
const isomorphicRequestCookies = isomorphicRequest ? parseIsomorphicRequestCookies(isomorphicRequest) : undefined;
const isomorphicRequestSearchParams = isomorphicRequest?.url
? new URL(isomorphicRequest.url)?.searchParams
: undefined;
const { cookies, headers, searchParams } = buildRequest(options?.request);

options = {
...options,
frontendApi: parsePublishableKey(options.publishableKey)?.frontendApi || options.frontendApi,
apiUrl: options.apiUrl || API_URL,
apiVersion: options.apiVersion || API_VERSION,
headerToken:
stripAuthorizationHeader(options.headerToken) ||
stripAuthorizationHeader(isomorphicRequest?.headers?.get(constants.Headers.Authorization)) ||
undefined,
cookieToken: options.cookieToken || isomorphicRequestCookies?.(constants.Cookies.Session) || undefined,
clientUat: options.clientUat || isomorphicRequestCookies?.(constants.Cookies.ClientUat) || undefined,
origin: options.origin || isomorphicRequest?.headers?.get(constants.Headers.Origin) || undefined,
host: options.host || isomorphicRequest?.headers?.get(constants.Headers.Host) || undefined,
forwardedHost:
options.forwardedHost || isomorphicRequest?.headers?.get(constants.Headers.ForwardedHost) || undefined,
forwardedPort:
options.forwardedPort || isomorphicRequest?.headers?.get(constants.Headers.ForwardedPort) || undefined,
forwardedProto:
options.forwardedProto || isomorphicRequest?.headers?.get(constants.Headers.ForwardedProto) || undefined,
referrer: options.referrer || isomorphicRequest?.headers?.get(constants.Headers.Referrer) || undefined,
userAgent: options.userAgent || isomorphicRequest?.headers?.get(constants.Headers.UserAgent) || undefined,
searchParams: options.searchParams || isomorphicRequestSearchParams || undefined,
headerToken: stripAuthorizationHeader(options.headerToken || headers?.(constants.Headers.Authorization)),
cookieToken: options.cookieToken || cookies?.(constants.Cookies.Session),
clientUat: options.clientUat || cookies?.(constants.Cookies.ClientUat),
origin: options.origin || headers?.(constants.Headers.Origin),
host: options.host || headers?.(constants.Headers.Host),
forwardedHost: options.forwardedHost || headers?.(constants.Headers.ForwardedHost),
forwardedPort: options.forwardedPort || headers?.(constants.Headers.ForwardedPort),
forwardedProto: options.forwardedProto || headers?.(constants.Headers.ForwardedProto),
referrer: options.referrer || headers?.(constants.Headers.Referrer),
userAgent: options.userAgent || headers?.(constants.Headers.UserAgent),
searchParams: options.searchParams || searchParams || undefined,
};

assertValidSecretKey(options.secretKey || options.apiKey);
Expand Down Expand Up @@ -212,7 +202,3 @@ export const debugRequestState = (params: RequestState) => {
};

export type DebugRequestSate = ReturnType<typeof debugRequestState>;

const stripAuthorizationHeader = (authValue: string | undefined | null): string | undefined => {
return authValue?.replace('Bearer ', '');
};
37 changes: 25 additions & 12 deletions packages/backend/src/util/IsomorphicRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,24 @@ import { parse } from 'cookie';

import runtime from '../runtime';

type IsomorphicRequestOptions = {
headers?: Record<string, string> | any;
type IsomorphicRequestOptions = (Request: typeof runtime.Request, Headers: typeof runtime.Headers) => Request;
export const createIsomorphicRequest = (cb: IsomorphicRequestOptions): Request => {
return cb(runtime.Request, runtime.Headers);
};

export const createIsomorphicRequest = (url: string | URL, reqOpts?: IsomorphicRequestOptions): Request => {
const headers = reqOpts?.headers;
// if (!!reqOpts?.headers && typeof headers.forEach === 'function') {
// headers = {};
// reqOpts?.headers.forEach((value: string, key: string) => {
// headers = Object.assign(headers, { [key]: value });
// });
// }
return new runtime.Request(url, { ...reqOpts, headers });
export const buildRequest = (req?: Request) => {
if (!req) {
return {};
}
const cookies = parseIsomorphicRequestCookies(req);
const headers = getHeaderFromIsomorphicRequest(req);
const searchParams = getSearchParamsFromIsomorphicRequest(req);

return {
cookies,
headers,
searchParams,
};
};

const decode = (str: string): string => {
Expand All @@ -24,7 +29,7 @@ const decode = (str: string): string => {
return str.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent);
};

export const parseIsomorphicRequestCookies = (req: Request) => {
const parseIsomorphicRequestCookies = (req: Request) => {
const cookies = req.headers && req.headers?.get('cookie') ? parse(req.headers.get('cookie') as string) : {};
return (key: string): string | undefined => {
const value = cookies?.[key];
Expand All @@ -34,3 +39,11 @@ export const parseIsomorphicRequestCookies = (req: Request) => {
return decode(value);
};
};

const getHeaderFromIsomorphicRequest = (req: Request) => (key: string) => req?.headers?.get(key) || undefined;

const getSearchParamsFromIsomorphicRequest = (req: Request) => (req?.url ? new URL(req.url)?.searchParams : undefined);

export const stripAuthorizationHeader = (authValue: string | undefined | null): string | undefined => {
return authValue?.replace('Bearer ', '');
};
24 changes: 3 additions & 21 deletions packages/fastify/src/withClerkMiddleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,7 @@ describe('withClerkMiddleware(options)', () => {
expect.objectContaining({
secretKey: 'TEST_API_KEY',
apiKey: 'TEST_API_KEY',
headerToken: 'deadbeef',
cookieToken: undefined,
clientUat: undefined,
origin: 'http://origin.com',
host: 'host.com',
forwardedPort: '1234',
forwardedHost: 'forwarded-host.com',
referrer: 'referer.com',
userAgent: 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
request: expect.any(Request),
}),
);
});
Expand Down Expand Up @@ -107,15 +99,7 @@ describe('withClerkMiddleware(options)', () => {
expect.objectContaining({
secretKey: 'TEST_API_KEY',
apiKey: 'TEST_API_KEY',
cookieToken: 'deadbeef',
headerToken: undefined,
clientUat: '1675692233',
origin: 'http://origin.com',
host: 'host.com',
forwardedPort: '1234',
forwardedHost: 'forwarded-host.com',
referrer: 'referer.com',
userAgent: 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
request: expect.any(Request),
}),
);
});
Expand Down Expand Up @@ -211,9 +195,7 @@ describe('withClerkMiddleware(options)', () => {
expect.objectContaining({
secretKey: 'TEST_API_KEY',
apiKey: 'TEST_API_KEY',
headerToken: 'deadbeef',
cookieToken: undefined,
clientUat: undefined,
request: expect.any(Request),
}),
);
});
Expand Down
74 changes: 71 additions & 3 deletions packages/fastify/src/withClerkMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import * as constants from './constants';
import type { ClerkFastifyOptions } from './types';
import { getSingleValueFromArrayHeader } from './utils';

const DUMMY_URL_BASE = 'http://clerk-dummy';

export const withClerkMiddleware = (options: ClerkFastifyOptions) => {
return async (req: FastifyRequest, reply: FastifyReply) => {
const secretKey = options.secretKey || constants.SECRET_KEY;
Expand All @@ -17,9 +19,30 @@ export const withClerkMiddleware = (options: ClerkFastifyOptions) => {
publishableKey,
apiKey: constants.API_KEY,
frontendApi: constants.FRONTEND_API,
forwardedHost: getSingleValueFromArrayHeader(req.headers?.[constants.Headers.ForwardedHost]),
forwardedPort: getSingleValueFromArrayHeader(req.headers?.[constants.Headers.ForwardedPort]),
request: createIsomorphicRequest(req.url, { headers: req.headers }),
request: createIsomorphicRequest((Request, Headers) => {
const requestHeaders = Object.keys(req.headers).reduce(
(acc, key) => Object.assign(acc, { [key]: req?.headers[key] }),
{},
);
const headers = new Headers(requestHeaders);
const forwardedHostHeader = getSingleValueFromArrayHeader(
headers?.get(constants.Headers.ForwardedHost) || undefined,
);
if (forwardedHostHeader) {
headers.set(constants.Headers.ForwardedHost, forwardedHostHeader);
}
const forwardedPortHeader = getSingleValueFromArrayHeader(
headers?.get(constants.Headers.ForwardedPort) || undefined,
);
if (forwardedPortHeader) {
headers.set(constants.Headers.ForwardedPort, forwardedPortHeader);
}
const reqUrl = isRelativeUrl(req.url) ? getAbsoluteUrlFromHeaders(req.url, headers) : req.url;
return new Request(reqUrl, {
method: req.method,
headers,
});
}),
});

// Interstitial cases
Expand Down Expand Up @@ -49,3 +72,48 @@ export const withClerkMiddleware = (options: ClerkFastifyOptions) => {
req.auth = requestState.toAuth();
};
};

// TODO: Move the utils below to shared package
// Creating a Request object requires a valid absolute URL
// Fastify's req.url is relative, so we need to construct an absolute URL
const getAbsoluteUrlFromHeaders = (url: string, headers: Headers): URL => {
const forwardedProto = headers.get(constants.Headers.ForwardedProto);
const forwardedPort = headers.get(constants.Headers.ForwardedPort);
const forwardedHost = headers.get(constants.Headers.ForwardedHost);

const fwdProto = getFirstValueFromHeaderValue(forwardedProto);
let fwdPort = getFirstValueFromHeaderValue(forwardedPort);

// If forwardedPort mismatch with forwardedProto determine forwardedPort
// from forwardedProto as fallback (if exists)
// This check fixes the Railway App issue
const fwdProtoHasMoreValuesThanFwdPorts =
(forwardedProto || '').split(',').length > (forwardedPort || '').split(',').length;
if (fwdProto && fwdProtoHasMoreValuesThanFwdPorts) {
fwdPort = getPortFromProtocol(fwdProto);
}

try {
return new URL(url, `${fwdProto}://${forwardedHost}${fwdPort ? ':' + fwdPort : ''}`);
} catch (e) {
return new URL(url, DUMMY_URL_BASE);
}
};

const PROTOCOL_TO_PORT_MAPPING: Record<string, string> = {
http: '80',
https: '443',
} as const;

function getPortFromProtocol(protocol: string) {
return PROTOCOL_TO_PORT_MAPPING[protocol];
}

function getFirstValueFromHeaderValue(value?: string | null) {
return value?.split(',')[0]?.trim() || '';
}

const isRelativeUrl = (url: string) => {
const u = new URL(url, DUMMY_URL_BASE);
return u.origin === DUMMY_URL_BASE;
};
13 changes: 11 additions & 2 deletions packages/gatsby-plugin-clerk/src/ssr/authenticateRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,17 @@ export function authenticateRequest(context: GetServerDataProps, options: WithSe
secretKey: SECRET_KEY,
frontendApi: FRONTEND_API,
publishableKey: PUBLISHABLE_KEY,
forwardedHost: returnReferrerAsXForwardedHostToFixLocalDevGatsbyProxy(context.headers),
request: createIsomorphicRequest(context.url, { headers: context.headers }),
request: createIsomorphicRequest((Request, Headers) => {
const headers = new Headers(Object.fromEntries(context.headers) as Record<string, string>);
headers.set(
constants.Headers.ForwardedHost,
returnReferrerAsXForwardedHostToFixLocalDevGatsbyProxy(context.headers),
);
return new Request(context.url, {
method: context.method,
headers,
});
}),
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ exports[`/api public exports should not include a breaking change 1`] = `
"constants",
"createAuthenticateRequest",
"createClerkClient",
"createIsomorphicRequest",
"debugRequestState",
"decodeJwt",
"deserialize",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ exports[`/edge-middleware public exports should not include a breaking change 1`
"constants",
"createAuthenticateRequest",
"createClerkClient",
"createIsomorphicRequest",
"debugRequestState",
"decodeJwt",
"deserialize",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ exports[`/server public exports should not include a breaking change 1`] = `
"constants",
"createAuthenticateRequest",
"createClerkClient",
"createIsomorphicRequest",
"currentUser",
"debugRequestState",
"decodeJwt",
Expand Down
3 changes: 1 addition & 2 deletions packages/nextjs/src/server/authenticateRequest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { createIsomorphicRequest } from '@clerk/backend';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';

Expand Down Expand Up @@ -29,7 +28,7 @@ export const authenticateRequest = async (req: NextRequest, opts: WithAuthOption
domain,
signInUrl,
proxyUrl,
request: createIsomorphicRequest(req.url, { headers: req.headers }),
request: req,
});
};

Expand Down
7 changes: 6 additions & 1 deletion packages/remix/src/ssr/authenticateRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ export function authenticateRequest(args: LoaderFunctionArgs, opts: RootAuthLoad
isSatellite,
domain,
signInUrl,
request: createIsomorphicRequest(requestURL, { headers: request.headers }),
request: createIsomorphicRequest((Request, Headers) => {
return new Request(requestURL, {
method: request.method,
headers: new Headers(request.headers),
});
}),
});
}
2 changes: 0 additions & 2 deletions packages/sdk-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
"url": "https://github.com/clerkinc/clerk-sdk-node"
},
"devDependencies": {
"@types/cookie": "^0.5.0",
"nock": "^13.0.7",
"npm-run-all": "^4.1.5",
"prettier": "^2.5.0",
Expand All @@ -65,7 +64,6 @@
"@types/express": "4.17.14",
"@types/node-fetch": "2.6.2",
"camelcase-keys": "6.2.2",
"cookie": "0.5.0",
"snakecase-keys": "3.2.1",
"tslib": "2.4.1"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ exports[`module exports should not change unless explicitly set 1`] = `
"createClerkClient",
"createClerkExpressRequireAuth",
"createClerkExpressWithAuth",
"createIsomorphicRequest",
"debugRequestState",
"decodeJwt",
"default",
Expand Down
Loading

0 comments on commit 04045b1

Please sign in to comment.