diff --git a/.changeset/kind-onions-think.md b/.changeset/kind-onions-think.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/kind-onions-think.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/backend/.eslintrc.js b/packages/backend/.eslintrc.js index de32b51322..c8eaf6aad8 100644 --- a/packages/backend/.eslintrc.js +++ b/packages/backend/.eslintrc.js @@ -10,6 +10,7 @@ module.exports = { rules: { // TODO: It's an issue specific to QUnit tests '@typescript-eslint/unbound-method': 'off', + '@typescript-eslint/no-unsafe-declaration-merging': 'off', }, }, ], diff --git a/packages/backend/src/__tests__/exports.test.ts b/packages/backend/src/__tests__/exports.test.ts index c4eea7ff7c..cfb3307781 100644 --- a/packages/backend/src/__tests__/exports.test.ts +++ b/packages/backend/src/__tests__/exports.test.ts @@ -32,10 +32,9 @@ export default (QUnit: QUnit) => { test('should not include a breaking change', assert => { const exportedApiKeys = [ 'AuthStatus', - 'buildRequestUrl', 'constants', 'createAuthenticateRequest', - 'createIsomorphicRequest', + 'createClerkRequest', 'debugRequestState', 'decorateObjectWithResources', 'makeAuthObjectSerializable', diff --git a/packages/backend/src/__tests__/utils.test.ts b/packages/backend/src/__tests__/utils.test.ts deleted file mode 100644 index de8e53dd28..0000000000 --- a/packages/backend/src/__tests__/utils.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type QUnit from 'qunit'; - -import { buildOrigin, buildRequestUrl } from '../utils'; - -export default (QUnit: QUnit) => { - const { module, test } = QUnit; - - module('buildOrigin({ protocol, forwardedProto, forwardedHost, host })', () => { - test('without any param', assert => { - assert.equal(buildOrigin({}), ''); - }); - - test('with protocol', assert => { - assert.equal(buildOrigin({ protocol: 'http' }), ''); - }); - - test('with host', assert => { - assert.equal(buildOrigin({ host: 'localhost:3000' }), ''); - }); - - test('with protocol and host', assert => { - assert.equal(buildOrigin({ protocol: 'http', host: 'localhost:3000' }), 'http://localhost:3000'); - }); - - test('with forwarded proto', assert => { - assert.equal( - buildOrigin({ protocol: 'http', host: 'localhost:3000', forwardedProto: 'https' }), - 'https://localhost:3000', - ); - }); - - test('with forwarded proto - with multiple values', assert => { - assert.equal( - buildOrigin({ protocol: 'http', host: 'localhost:3000', forwardedProto: 'https,http' }), - 'https://localhost:3000', - ); - }); - - test('with forwarded host', assert => { - assert.equal( - buildOrigin({ protocol: 'http', host: 'localhost:3000', forwardedHost: 'example.com' }), - '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' }), - 'http://example.com', - ); - }); - - test('with forwarded proto and host', assert => { - assert.equal( - buildOrigin({ - protocol: 'http', - host: 'localhost:3000', - forwardedProto: 'https', - forwardedHost: 'example.com', - }), - 'https://example.com', - ); - }); - - test('with forwarded proto and host - without protocol', assert => { - assert.equal( - buildOrigin({ host: 'localhost:3000', forwardedProto: 'https', forwardedHost: 'example.com' }), - 'https://example.com', - ); - }); - - test('with forwarded proto and host - without host', assert => { - assert.equal( - buildOrigin({ protocol: 'http', forwardedProto: 'https', forwardedHost: 'example.com' }), - 'https://example.com', - ); - }); - - test('with forwarded proto and host - without host and protocol', assert => { - assert.equal(buildOrigin({ forwardedProto: 'https', forwardedHost: 'example.com' }), 'https://example.com'); - }); - }); - - module('buildRequestUrl({ request, path })', () => { - test('without headers', assert => { - const req = new Request('http://localhost:3000/path'); - assert.equal(buildRequestUrl(req), '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), '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), 'https://example.com/path'); - }); - - test('with query params in request', assert => { - const req = new Request('http://localhost:3000/path'); - assert.equal(buildRequestUrl(req), 'http://localhost:3000/path'); - }); - }); -}; diff --git a/packages/backend/src/internal.ts b/packages/backend/src/internal.ts index ae3696eabd..ade7685255 100644 --- a/packages/backend/src/internal.ts +++ b/packages/backend/src/internal.ts @@ -1,13 +1,12 @@ export { constants } from './constants'; export { redirect } from './redirections'; -export { buildRequestUrl } from './utils'; export type { CreateAuthenticateRequestOptions } from './tokens/factory'; export { createAuthenticateRequest } from './tokens/factory'; export { debugRequestState } from './tokens/request'; -export type { AuthenticateRequestOptions, OptionalVerifyTokenOptions } from './tokens/request'; +export type { AuthenticateRequestOptions } from './tokens/types'; export type { SignedInAuthObjectOptions, @@ -16,9 +15,11 @@ export type { AuthObject, } from './tokens/authObjects'; export { makeAuthObjectSerializable, signedOutAuthObject, signedInAuthObject } from './tokens/authObjects'; -export { createIsomorphicRequest } from './util/IsomorphicRequest'; export { AuthStatus } from './tokens/authStatus'; export type { RequestState, SignedInState, SignedOutState } from './tokens/authStatus'; export { decorateObjectWithResources, stripPrivateDataFromObject } from './util/decorateObjectWithResources'; + +export { createClerkRequest } from './tokens/clerkRequest'; +export type { ClerkRequest } from './tokens/clerkRequest'; diff --git a/packages/backend/src/jwt/verifyJwt.ts b/packages/backend/src/jwt/verifyJwt.ts index acd48b4007..c63d25eef7 100644 --- a/packages/backend/src/jwt/verifyJwt.ts +++ b/packages/backend/src/jwt/verifyJwt.ts @@ -100,8 +100,9 @@ export type VerifyJwtOptions = { export async function verifyJwt( token: string, - { audience, authorizedParties, clockSkewInMs, key }: VerifyJwtOptions, + options: VerifyJwtOptions, ): Promise> { + const { audience, authorizedParties, clockSkewInMs, key } = options; const clockSkew = clockSkewInMs || DEFAULT_CLOCK_SKEW_IN_SECONDS; const { data: decoded, error } = decodeJwt(token); diff --git a/packages/backend/src/tokens/__tests__/clerkRequest.test.ts b/packages/backend/src/tokens/__tests__/clerkRequest.test.ts new file mode 100644 index 0000000000..0f23605b09 --- /dev/null +++ b/packages/backend/src/tokens/__tests__/clerkRequest.test.ts @@ -0,0 +1,116 @@ +import { createClerkRequest } from '../clerkRequest'; + +export default (QUnit: QUnit) => { + const { module, test: it } = QUnit; + + module('createClerkRequest', () => { + module('cookies', () => { + it('should parse and return cookies', assert => { + const req = createClerkRequest( + new Request('http://localhost:3000', { headers: new Headers({ cookie: 'foo=bar' }) }), + ); + assert.equal(req.cookies.get('foo'), 'bar'); + }); + + it('should parse and return cookies with special characters', assert => { + const req = createClerkRequest( + new Request('http://localhost:3000', { headers: new Headers({ cookie: 'foo=%20bar%3B%20baz%3Dqux' }) }), + ); + assert.equal(req.cookies.get('foo'), 'bar'); + assert.equal(req.cookies.get('baz'), 'qux'); + }); + + it('should parse and return cookies even if no cookie header exists', assert => { + const req = createClerkRequest(new Request('http://localhost:3000', { headers: new Headers() })); + assert.equal(req.cookies.get('foo'), undefined); + }); + + it('should parse and return cookies even if cookie header is empty', assert => { + const req = createClerkRequest(new Request('http://localhost:3000', { headers: new Headers({ cookie: '' }) })); + assert.equal(req.cookies.get('foo'), undefined); + }); + }); + + module('clerkUrl', () => { + it('should return a clerkUrl', assert => { + const req = createClerkRequest(new Request('http://localhost:3000')); + assert.equal(req.clerkUrl.href, 'http://localhost:3000/'); + }); + + it('without headers', assert => { + const req = new Request('http://localhost:3000/path'); + assert.equal(createClerkRequest(req).clerkUrl.toString(), 'http://localhost:3000/path'); + }); + + it('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(createClerkRequest(req).clerkUrl.toString(), 'https://example.com/path'); + }); + + it('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(createClerkRequest(req).clerkUrl.toString(), 'https://example.com/path'); + }); + + it('with path in request', assert => { + const req = new Request('http://localhost:3000/path'); + assert.equal(createClerkRequest(req).clerkUrl.toString(), 'http://localhost:3000/path'); + }); + + it('with query params in request', assert => { + const req = new Request('http://localhost:3000/path?foo=bar'); + assert.equal(createClerkRequest(req).clerkUrl.toString(), 'http://localhost:3000/path?foo=bar'); + }); + + it('with forwarded host (behind a proxy)', assert => { + const req = new Request('http://localhost:3000/path?foo=bar', { + headers: new Headers({ 'x-forwarded-host': 'example.com' }), + }); + assert.equal(createClerkRequest(req).clerkUrl.toString(), 'http://example.com/path?foo=bar'); + }); + + it('with forwarded host - with multiple values', assert => { + const req = new Request('http://localhost:3000/path?foo=bar', { + headers: { 'x-forwarded-host': 'example.com,example-2.com' }, + }); + assert.equal(createClerkRequest(req).clerkUrl.toString(), 'http://example.com/path?foo=bar'); + }); + + it('with forwarded proto and host', assert => { + const req = new Request('http://localhost:3000/path?foo=bar', { + headers: { 'x-forwarded-host': 'example.com', 'x-forwarded-proto': 'https' }, + }); + assert.equal(createClerkRequest(req).clerkUrl.toString(), 'https://example.com/path?foo=bar'); + }); + + it('with forwarded proto and host - without protocol', assert => { + const req = new Request('http://localhost:3000/path?foo=bar', { + headers: { 'x-forwarded-host': 'example.com', 'x-forwarded-proto': 'https' }, + }); + assert.equal(createClerkRequest(req).clerkUrl.toString(), 'https://example.com/path?foo=bar'); + }); + + it('with forwarded proto and host - without host', assert => { + const req = new Request('http://localhost:3000/path?foo=bar', { + headers: { 'x-forwarded-host': 'example.com', 'x-forwarded-proto': 'https' }, + }); + assert.equal(createClerkRequest(req).clerkUrl.toString(), 'https://example.com/path?foo=bar'); + }); + + it('with forwarded proto and host - without host and protocol', assert => { + const req = new Request('http://localhost:3000/path?foo=bar', { + headers: { 'x-forwarded-host': 'example.com', 'x-forwarded-proto': 'https' }, + }); + assert.equal(createClerkRequest(req).clerkUrl.toString(), 'https://example.com/path?foo=bar'); + }); + }); + }); +}; diff --git a/packages/backend/src/tokens/__tests__/request.test.ts b/packages/backend/src/tokens/__tests__/request.test.ts index ad65b03aad..b2710cba14 100644 --- a/packages/backend/src/tokens/__tests__/request.test.ts +++ b/packages/backend/src/tokens/__tests__/request.test.ts @@ -6,8 +6,8 @@ import { mockInvalidSignatureJwt, mockJwks, mockJwt, mockJwtPayload, mockMalform import runtime from '../../runtime'; import { jsonOk } from '../../util/testUtils'; import { AuthErrorReason, type AuthReason, AuthStatus, type RequestState } from '../authStatus'; -import type { AuthenticateRequestOptions } from '../request'; -import { authenticateRequest, loadOptionsFromHeaders } from '../request'; +import { authenticateRequest } from '../request'; +import type { AuthenticateRequestOptions } from '../types'; function assertSignedOut( assert, @@ -530,31 +530,4 @@ export default (QUnit: QUnit) => { assertSignedInToAuth(assert, requestState); }); }); - - module('tokens.loadOptionsFromHeaders', () => { - test('returns forwarded headers from headers', assert => { - const headersData = { 'x-forwarded-proto': 'http', 'x-forwarded-port': '80', 'x-forwarded-host': 'example.com' }; - const headers = key => headersData[key] || ''; - - assert.propContains(loadOptionsFromHeaders(headers), { - forwardedProto: 'http', - forwardedHost: 'example.com', - }); - }); - - test('returns Cloudfront forwarded proto from headers even if forwarded proto header exists', assert => { - const headersData = { - 'cloudfront-forwarded-proto': 'https', - 'x-forwarded-proto': 'http', - 'x-forwarded-port': '80', - 'x-forwarded-host': 'example.com', - }; - const headers = key => headersData[key] || ''; - - assert.propContains(loadOptionsFromHeaders(headers), { - forwardedProto: 'https', - forwardedHost: 'example.com', - }); - }); - }); }; diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index 9695f056cb..549b29601d 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -10,9 +10,9 @@ import type { import type { CreateBackendApiOptions } from '../api'; import { createBackendApiClient } from '../api'; +import type { AuthenticateContext } from './authenticateContext'; type AuthObjectDebugData = Record; -type CreateAuthObjectDebug = (data?: AuthObjectDebugData) => AuthObjectDebug; type AuthObjectDebug = () => AuthObjectDebugData; /** @@ -64,9 +64,9 @@ export type SignedOutAuthObject = { */ export type AuthObject = SignedInAuthObject | SignedOutAuthObject; -const createDebug: CreateAuthObjectDebug = data => { +const createDebug = (data: AuthObjectDebugData | undefined) => { return () => { - const res = { ...data } || {}; + const res = { ...data }; res.secretKey = (res.secretKey || '').substring(0, 7); res.jwtKey = (res.jwtKey || '').substring(0, 7); return { ...res }; @@ -77,9 +77,8 @@ const createDebug: CreateAuthObjectDebug = data => { * @internal */ export function signedInAuthObject( + authenticateContext: AuthenticateContext, sessionClaims: JwtPayload, - options: SignedInAuthObjectOptions, - debugData?: AuthObjectDebugData, ): SignedInAuthObject { const { act: actor, @@ -90,17 +89,11 @@ export function signedInAuthObject( org_permissions: orgPermissions, sub: userId, } = sessionClaims; - const { secretKey, apiUrl, apiVersion, token } = options; - const { sessions } = createBackendApiClient({ - secretKey, - apiUrl, - apiVersion, - }); - + const apiClient = createBackendApiClient(authenticateContext); const getToken = createGetToken({ sessionId, - sessionToken: token, - fetcher: (...args) => sessions.getToken(...args), + sessionToken: authenticateContext.sessionToken || '', + fetcher: (...args) => apiClient.sessions.getToken(...args), }); return { @@ -114,7 +107,7 @@ export function signedInAuthObject( orgPermissions, getToken, has: createHasAuthorization({ orgId, orgRole, orgPermissions, userId }), - debug: createDebug({ ...options, ...debugData }), + debug: createDebug({ ...authenticateContext }), }; } @@ -175,19 +168,15 @@ const createGetToken: CreateGetToken = params => { }; }; -const createHasAuthorization = - ({ - orgId, - orgRole, - userId, - orgPermissions, - }: { - userId: string; - orgId: string | undefined; - orgRole: string | undefined; - orgPermissions: string[] | undefined; - }): CheckAuthorizationWithCustomPermissions => - params => { +const createHasAuthorization = (options: { + userId: string; + orgId: string | undefined; + orgRole: string | undefined; + orgPermissions: string[] | undefined; +}): CheckAuthorizationWithCustomPermissions => { + const { orgId, orgRole, userId, orgPermissions } = options; + + return params => { if (!params?.permission && !params?.role) { throw new Error( 'Missing parameters. `has` from `auth` or `getAuth` requires a permission or role key to be passed. Example usage: `has({permission: "org:posts:edit"`', @@ -208,3 +197,4 @@ const createHasAuthorization = return false; }; +}; diff --git a/packages/backend/src/tokens/authStatus.ts b/packages/backend/src/tokens/authStatus.ts index 84fce17bcb..2a914ef93a 100644 --- a/packages/backend/src/tokens/authStatus.ts +++ b/packages/backend/src/tokens/authStatus.ts @@ -1,12 +1,10 @@ import type { JwtPayload } from '@clerk/types'; import type { TokenVerificationErrorReason } from '../errors'; -import type { SignedInAuthObject, SignedInAuthObjectOptions, SignedOutAuthObject } from './authObjects'; +import type { AuthenticateContext } from './authenticateContext'; +import type { SignedInAuthObject, SignedOutAuthObject } from './authObjects'; import { signedInAuthObject, signedOutAuthObject } from './authObjects'; -/** - * @internal - */ export enum AuthStatus { SignedIn = 'signed-in', SignedOut = 'signed-out', @@ -67,146 +65,74 @@ export enum AuthErrorReason { export type AuthReason = AuthErrorReason | TokenVerificationErrorReason; -/** - * @internal - */ export type RequestState = SignedInState | SignedOutState | HandshakeState; -type RequestStateParams = { - publishableKey?: string; - domain?: string; - isSatellite?: boolean; - proxyUrl?: string; - signInUrl?: string; - signUpUrl?: string; - afterSignInUrl?: string; - afterSignUpUrl?: string; -}; - -type AuthParams = { - /* Session token cookie value */ - sessionTokenInCookie?: string; - /* Client token header value */ - sessionTokenInHeader?: string; - /* Client uat cookie value */ - clientUat?: string; -}; - -export type AuthStatusOptionsType = Partial & RequestStateParams & AuthParams; - -export function signedIn( - options: T, +export function signedIn( + authenticateContext: AuthenticateContext, sessionClaims: JwtPayload, headers: Headers = new Headers(), ): SignedInState { - const { - publishableKey = '', - proxyUrl = '', - isSatellite = false, - domain = '', - signInUrl = '', - signUpUrl = '', - afterSignInUrl = '', - afterSignUpUrl = '', - secretKey, - apiUrl, - apiVersion, - sessionTokenInCookie, - sessionTokenInHeader, - } = options; - - const authObject = signedInAuthObject( - sessionClaims, - { - secretKey, - apiUrl, - apiVersion, - token: sessionTokenInCookie || sessionTokenInHeader || '', - }, - { ...options, status: AuthStatus.SignedIn }, - ); - + const authObject = signedInAuthObject(authenticateContext, sessionClaims); return { status: AuthStatus.SignedIn, reason: null, message: null, - proxyUrl, - publishableKey, - domain, - isSatellite, - signInUrl, - signUpUrl, - afterSignInUrl, - afterSignUpUrl, + proxyUrl: authenticateContext.proxyUrl || '', + publishableKey: authenticateContext.publishableKey || '', + isSatellite: authenticateContext.isSatellite || false, + domain: authenticateContext.domain || '', + signInUrl: authenticateContext.signInUrl || '', + signUpUrl: authenticateContext.signUpUrl || '', + afterSignInUrl: authenticateContext.afterSignInUrl || '', + afterSignUpUrl: authenticateContext.afterSignUpUrl || '', isSignedIn: true, toAuth: () => authObject, headers, }; } -export function signedOut( - options: T, + +export function signedOut( + authenticateContext: AuthenticateContext, reason: AuthReason, message = '', headers: Headers = new Headers(), ): SignedOutState { - const { - publishableKey = '', - proxyUrl = '', - isSatellite = false, - domain = '', - signInUrl = '', - signUpUrl = '', - afterSignInUrl = '', - afterSignUpUrl = '', - } = options; - return { status: AuthStatus.SignedOut, reason, message, - proxyUrl, - publishableKey, - isSatellite, - domain, - signInUrl, - signUpUrl, - afterSignInUrl, - afterSignUpUrl, + proxyUrl: authenticateContext.proxyUrl || '', + publishableKey: authenticateContext.publishableKey || '', + isSatellite: authenticateContext.isSatellite || false, + domain: authenticateContext.domain || '', + signInUrl: authenticateContext.signInUrl || '', + signUpUrl: authenticateContext.signUpUrl || '', + afterSignInUrl: authenticateContext.afterSignInUrl || '', + afterSignUpUrl: authenticateContext.afterSignUpUrl || '', isSignedIn: false, headers, - toAuth: () => signedOutAuthObject({ ...options, status: AuthStatus.SignedOut, reason, message }), + toAuth: () => signedOutAuthObject({ ...authenticateContext, status: AuthStatus.SignedOut, reason, message }), }; } -export function handshake( - options: T, +export function handshake( + authenticateContext: AuthenticateContext, reason: AuthReason, message = '', headers: Headers, ): HandshakeState { - const { - publishableKey = '', - proxyUrl = '', - isSatellite = false, - domain = '', - signInUrl = '', - signUpUrl = '', - afterSignInUrl = '', - afterSignUpUrl = '', - } = options; - return { status: AuthStatus.Handshake, reason, message, - publishableKey, - isSatellite, - domain, - proxyUrl, - signInUrl, - signUpUrl, - afterSignInUrl, - afterSignUpUrl, + publishableKey: authenticateContext.publishableKey || '', + isSatellite: authenticateContext.isSatellite || false, + domain: authenticateContext.domain || '', + proxyUrl: authenticateContext.proxyUrl || '', + signInUrl: authenticateContext.signInUrl || '', + signUpUrl: authenticateContext.signUpUrl || '', + afterSignInUrl: authenticateContext.afterSignInUrl || '', + afterSignUpUrl: authenticateContext.afterSignUpUrl || '', isSignedIn: false, headers, toAuth: () => null, diff --git a/packages/backend/src/tokens/authenticateContext.ts b/packages/backend/src/tokens/authenticateContext.ts new file mode 100644 index 0000000000..7ecf5f4849 --- /dev/null +++ b/packages/backend/src/tokens/authenticateContext.ts @@ -0,0 +1,88 @@ +import { constants } from '../constants'; +import type { ClerkRequest } from './clerkRequest'; +import type { AuthenticateRequestOptions } from './types'; + +interface AuthenticateContextInterface extends AuthenticateRequestOptions { + // header-based values + sessionTokenInHeader: string | undefined; + origin: string | undefined; + host: string | undefined; + forwardedHost: string | undefined; + forwardedProto: string | undefined; + referrer: string | undefined; + userAgent: string | undefined; + secFetchDest: string | undefined; + accept: string | undefined; + // cookie-based values + sessionTokenInCookie: string | undefined; + clientUat: number; + // handshake-related values + devBrowserToken: string | undefined; + handshakeToken: string | undefined; + // url derived from headers + clerkUrl: URL; + // cookie or header session token + sessionToken: string | undefined; +} + +interface AuthenticateContext extends AuthenticateContextInterface {} + +/** + * All data required to authenticate a request. + * This is the data we use to decide whether a request + * is in a signed in or signed out state or if we need + * to perform a handshake. + */ +class AuthenticateContext { + public get sessionToken(): string | undefined { + return this.sessionTokenInCookie || this.sessionTokenInHeader; + } + + public constructor(private clerkRequest: ClerkRequest, options: AuthenticateRequestOptions) { + this.initHeaderValues(); + this.initCookieValues(); + this.initHandshakeValues(); + Object.assign(this, options); + this.clerkUrl = this.clerkRequest.clerkUrl; + } + + private initHandshakeValues() { + this.devBrowserToken = + this.clerkRequest.clerkUrl.searchParams.get(constants.Cookies.DevBrowser) || + this.clerkRequest.cookies.get(constants.Cookies.DevBrowser); + this.handshakeToken = + this.clerkRequest.clerkUrl.searchParams.get(constants.Cookies.Handshake) || + this.clerkRequest.cookies.get(constants.Cookies.Handshake); + } + + private initHeaderValues() { + const get = (name: string) => this.clerkRequest.headers.get(name) || undefined; + this.sessionTokenInHeader = this.stripAuthorizationHeader(get(constants.Headers.Authorization)); + this.origin = get(constants.Headers.Origin); + this.host = get(constants.Headers.Host); + this.forwardedHost = get(constants.Headers.ForwardedHost); + this.forwardedProto = get(constants.Headers.CloudFrontForwardedProto) || get(constants.Headers.ForwardedProto); + this.referrer = get(constants.Headers.Referrer); + this.userAgent = get(constants.Headers.UserAgent); + this.secFetchDest = get(constants.Headers.SecFetchDest); + this.accept = get(constants.Headers.Accept); + } + + private initCookieValues() { + const get = (name: string) => this.clerkRequest.cookies.get(name) || undefined; + this.sessionTokenInCookie = get(constants.Cookies.Session); + this.clientUat = Number.parseInt(get(constants.Cookies.ClientUat) || '') || 0; + } + + private stripAuthorizationHeader(authValue: string | undefined | null): string | undefined { + return authValue?.replace('Bearer ', ''); + } +} + +export type { AuthenticateContext }; + +export const createAuthenticateContext = ( + ...args: ConstructorParameters +): AuthenticateContext => { + return new AuthenticateContext(...args); +}; diff --git a/packages/backend/src/tokens/clerkRequest.ts b/packages/backend/src/tokens/clerkRequest.ts new file mode 100644 index 0000000000..41fe49d2d6 --- /dev/null +++ b/packages/backend/src/tokens/clerkRequest.ts @@ -0,0 +1,68 @@ +import { parse as parseCookies } from 'cookie'; + +import { constants } from '../constants'; + +export type WithClerkUrl = T & { + /** + * When a NextJs app is hosted on a platform different from Vercel + * or inside a container (Netlify, Fly.io, AWS Amplify, docker etc), + * req.url is always set to `localhost:3000` instead of the actual host of the app. + * + * The `authMiddleware` uses the value of the available req.headers in order to construct + * and use the correct url internally. This url is then exposed as `experimental_clerkUrl`, + * intended to be used within `beforeAuth` and `afterAuth` if needed. + */ + clerkUrl: URL; +}; + +class ClerkRequest extends Request { + readonly clerkUrl: URL; + readonly cookies: Map; + + public constructor(req: Request) { + super(req, req); + this.clerkUrl = this.deriveUrlFromHeaders(req); + this.cookies = this.parseCookies(req); + } + + public decorateWithClerkUrl = (req: R): WithClerkUrl => { + return Object.assign(req, { clerkUrl: this.clerkUrl }); + }; + + /** + * Used to fix request.url using the x-forwarded-* headers + * TODO add detailed description of the issues this solves + */ + private deriveUrlFromHeaders(req: Request) { + const initialUrl = new URL(req.url); + const forwardedProto = req.headers.get(constants.Headers.ForwardedProto); + const forwardedHost = req.headers.get(constants.Headers.ForwardedHost); + const host = req.headers.get(constants.Headers.Host); + const protocol = initialUrl.protocol; + + const resolvedHost = this.getFirstValueFromHeader(forwardedHost) ?? host; + const resolvedProtocol = this.getFirstValueFromHeader(forwardedProto) ?? protocol?.replace(/[:/]/, ''); + const origin = resolvedHost && resolvedProtocol ? `${resolvedProtocol}://${resolvedHost}` : initialUrl.origin; + + return new URL(initialUrl.pathname + initialUrl.search, origin); + } + + private getFirstValueFromHeader(value?: string | null) { + return value?.split(',')[0]; + } + + private parseCookies(req: Request) { + const cookiesRecord = parseCookies(this.decodeCookieValue(req.headers.get('cookie') || '')); + return new Map(Object.entries(cookiesRecord)); + } + + private decodeCookieValue(str: string) { + return str ? str.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent) : str; + } +} + +export const createClerkRequest = (...args: ConstructorParameters): ClerkRequest => { + return new ClerkRequest(...args); +}; + +export type { ClerkRequest }; diff --git a/packages/backend/src/tokens/factory.ts b/packages/backend/src/tokens/factory.ts index ed521cfdfb..3da0ae9b96 100644 --- a/packages/backend/src/tokens/factory.ts +++ b/packages/backend/src/tokens/factory.ts @@ -1,10 +1,9 @@ import type { ApiClient } from '../api'; import { mergePreDefinedOptions } from '../util/mergePreDefinedOptions'; -import type { AuthenticateRequestOptions } from './request'; import { authenticateRequest as authenticateRequestOriginal, debugRequestState } from './request'; +import type { AuthenticateRequestOptions } from './types'; type RunTimeOptions = Omit; - type BuildTimeOptions = Partial< Pick< AuthenticateRequestOptions, diff --git a/packages/backend/src/tokens/keys.ts b/packages/backend/src/tokens/keys.ts index 553da86f54..fa3bc9cbc8 100644 --- a/packages/backend/src/tokens/keys.ts +++ b/packages/backend/src/tokens/keys.ts @@ -9,7 +9,6 @@ import { // For more information refer to https://sinonjs.org/how-to/stub-dependency/ import runtime from '../runtime'; import { joinPaths } from '../util/path'; -import { getErrorObjectByCode } from '../util/request'; import { callWithRetry } from '../util/shared'; type JsonWebKeyWithKid = JsonWebKey & { kid: string }; @@ -211,3 +210,17 @@ async function fetchJWKSFromBAPI(apiUrl: string, key: string, apiVersion: string function cacheHasExpired() { return Date.now() - lastUpdatedAt >= MAX_CACHE_LAST_UPDATED_AT_SECONDS * 1000; } + +type ErrorFields = { + message: string; + long_message: string; + code: string; +}; + +const getErrorObjectByCode = (errors: ErrorFields[], code: string) => { + if (!errors) { + return null; + } + + return errors.find((err: ErrorFields) => err.code === code); +}; diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index f0df811a61..fb38360584 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -5,27 +5,15 @@ import type { TokenCarrier } from '../errors'; import { TokenVerificationError, TokenVerificationErrorReason } from '../errors'; import { decodeJwt } from '../jwt'; import { assertValidSecretKey } from '../util/assertValidSecretKey'; -import { buildRequest, stripAuthorizationHeader } from '../util/IsomorphicRequest'; import { isDevelopmentFromSecretKey } from '../util/shared'; -import type { AuthStatusOptionsType, RequestState } from './authStatus'; +import type { AuthenticateContext } from './authenticateContext'; +import { createAuthenticateContext } from './authenticateContext'; +import type { RequestState } from './authStatus'; import { AuthErrorReason, handshake, signedIn, signedOut } from './authStatus'; +import { createClerkRequest } from './clerkRequest'; import { verifyHandshakeToken } from './handshake'; -import { verifyToken, type VerifyTokenOptions } from './verify'; - -/** - * @internal - */ -export type OptionalVerifyTokenOptions = Partial< - Pick< - VerifyTokenOptions, - 'audience' | 'authorizedParties' | 'clockSkewInMs' | 'jwksCacheTtlInMs' | 'skipJwksCache' | 'jwtKey' - > ->; - -/** - * @internal - */ -export type AuthenticateRequestOptions = AuthStatusOptionsType & OptionalVerifyTokenOptions; +import type { AuthenticateRequestOptions } from './types'; +import { verifyToken } from './verify'; function assertSignInUrlExists(signInUrl: string | undefined, key: string): asserts signInUrl is string { if (!signInUrl && isDevelopmentFromSecretKey(key)) { @@ -75,20 +63,7 @@ export async function authenticateRequest( request: Request, options: AuthenticateRequestOptions, ): Promise { - const { cookies, headers, searchParams, derivedRequestUrl } = buildRequest(request); - - const authenticateContext = { - ...options, - ...loadOptionsFromHeaders(headers), - ...loadOptionsFromCookies(cookies), - searchParams, - derivedRequestUrl, - }; - - const devBrowserToken = - searchParams?.get(constants.Cookies.DevBrowser) || cookies(constants.Cookies.DevBrowser) || ''; - const handshakeToken = searchParams?.get(constants.Cookies.Handshake) || cookies(constants.Cookies.Handshake) || ''; - + const authenticateContext = createAuthenticateContext(createClerkRequest(request), options); assertValidSecretKey(authenticateContext.secretKey); if (authenticateContext.isSatellite) { @@ -100,29 +75,27 @@ export async function authenticateRequest( } function buildRedirectToHandshake() { - const redirectUrl = new URL(derivedRequestUrl); + const redirectUrl = new URL(authenticateContext.clerkUrl); redirectUrl.searchParams.delete('__clerk_db_jwt'); const frontendApiNoProtocol = pk.frontendApi.replace(/http(s)?:\/\//, ''); const url = new URL(`https://${frontendApiNoProtocol}/v1/client/handshake`); url.searchParams.append('redirect_url', redirectUrl?.href || ''); - if (pk?.instanceType === 'development' && devBrowserToken) { - url.searchParams.append('__clerk_db_jwt', devBrowserToken); + if (pk?.instanceType === 'development' && authenticateContext.devBrowserToken) { + url.searchParams.append('__clerk_db_jwt', authenticateContext.devBrowserToken); } return new Headers({ location: url.href }); } async function resolveHandshake() { - const { derivedRequestUrl } = authenticateContext; - const headers = new Headers({ 'Access-Control-Allow-Origin': 'null', 'Access-Control-Allow-Credentials': 'true', }); - const handshakePayload = await verifyHandshakeToken(handshakeToken, authenticateContext); + const handshakePayload = await verifyHandshakeToken(authenticateContext.handshakeToken!, authenticateContext); const cookiesToSet = handshakePayload.handshake; let sessionToken = ''; @@ -134,7 +107,7 @@ export async function authenticateRequest( }); if (instanceType === 'development') { - const newUrl = new URL(derivedRequestUrl); + const newUrl = new URL(authenticateContext.clerkUrl); newUrl.searchParams.delete('__clerk_handshake'); newUrl.searchParams.delete('__clerk_help'); headers.append('Location', newUrl.toString()); @@ -182,17 +155,17 @@ ${error.getFullMessage()}`, } function handleMaybeHandshakeStatus( - context: typeof authenticateContext, + authenticateContext: AuthenticateContext, reason: AuthErrorReason, message: string, headers?: Headers, ) { - if (isRequestEligibleForHandshake(context)) { + if (isRequestEligibleForHandshake(authenticateContext)) { // Right now the only usage of passing in different headers is for multi-domain sync, which redirects somewhere else. // In the future if we want to decorate the handshake redirect with additional headers per call we need to tweak this logic. - return handshake(context, reason, message, headers ?? buildRedirectToHandshake()); + return handshake(authenticateContext, reason, message, headers ?? buildRedirectToHandshake()); } - return signedOut(context, reason, message, new Headers()); + return signedOut(authenticateContext, reason, message, new Headers()); } const pk = parsePublishableKey(options.publishableKey, { @@ -212,42 +185,32 @@ ${error.getFullMessage()}`, throw error; } // use `await` to force this try/catch handle the signedIn invocation - return await signedIn(options, data); + return await signedIn(authenticateContext, data); } catch (err) { return handleError(err, 'header'); } } async function authenticateRequestWithTokenInCookie() { - const { - derivedRequestUrl, - isSatellite, - secFetchDest, - signInUrl, - clientUat: clientUatRaw, - sessionTokenInCookie: sessionToken, - } = authenticateContext; - - const clientUat = parseInt(clientUatRaw || '', 10) || 0; - const hasActiveClient = clientUat > 0; - const hasSessionToken = !!sessionToken; + const hasActiveClient = authenticateContext.clientUat; + const hasSessionToken = !!authenticateContext.sessionTokenInCookie; const isRequestEligibleForMultiDomainSync = - isSatellite && - secFetchDest === 'document' && - !derivedRequestUrl.searchParams.has(constants.QueryParameters.ClerkSynced); + authenticateContext.isSatellite && + authenticateContext.secFetchDest === 'document' && + !authenticateContext.clerkUrl.searchParams.has(constants.QueryParameters.ClerkSynced); /** * If we have a handshakeToken, resolve the handshake and attempt to return a definitive signed in or signed out state. */ - if (handshakeToken) { + if (authenticateContext.handshakeToken) { return resolveHandshake(); } /** * Otherwise, check for "known unknown" auth states that we can resolve with a handshake. */ - if (instanceType === 'development' && derivedRequestUrl.searchParams.has(constants.Cookies.DevBrowser)) { + if (instanceType === 'development' && authenticateContext.clerkUrl.searchParams.has(constants.Cookies.DevBrowser)) { return handleMaybeHandshakeStatus(authenticateContext, AuthErrorReason.DevBrowserSync, ''); } @@ -263,21 +226,29 @@ ${error.getFullMessage()}`, // initiate MD sync // signInUrl exists, checked at the top of `authenticateRequest` - const redirectURL = new URL(signInUrl!); - redirectURL.searchParams.append(constants.QueryParameters.ClerkRedirectUrl, derivedRequestUrl.toString()); + const redirectURL = new URL(authenticateContext.signInUrl!); + redirectURL.searchParams.append( + constants.QueryParameters.ClerkRedirectUrl, + authenticateContext.clerkUrl.toString(), + ); const headers = new Headers({ location: redirectURL.toString() }); return handleMaybeHandshakeStatus(authenticateContext, AuthErrorReason.SatelliteCookieNeedsSyncing, '', headers); } // Multi-domain development sync flow - const redirectUrl = new URL(derivedRequestUrl).searchParams.get(constants.QueryParameters.ClerkRedirectUrl); - if (instanceType === 'development' && !isSatellite && redirectUrl) { + const redirectUrl = new URL(authenticateContext.clerkUrl).searchParams.get( + constants.QueryParameters.ClerkRedirectUrl, + ); + if (instanceType === 'development' && !authenticateContext.isSatellite && redirectUrl) { // Dev MD sync from primary, redirect back to satellite w/ __clerk_db_jwt const redirectBackToSatelliteUrl = new URL(redirectUrl); - if (devBrowserToken) { - redirectBackToSatelliteUrl.searchParams.append(constants.Cookies.DevBrowser, devBrowserToken); + if (authenticateContext.devBrowserToken) { + redirectBackToSatelliteUrl.searchParams.append( + constants.Cookies.DevBrowser, + authenticateContext.devBrowserToken, + ); } redirectBackToSatelliteUrl.searchParams.append(constants.QueryParameters.ClerkSynced, 'true'); @@ -301,17 +272,17 @@ ${error.getFullMessage()}`, return handleMaybeHandshakeStatus(authenticateContext, AuthErrorReason.ClientUATWithoutSessionToken, ''); } - const { data: decodeResult, error: decodedError } = decodeJwt(sessionToken!); + const { data: decodeResult, error: decodedError } = decodeJwt(authenticateContext.sessionTokenInCookie!); if (decodedError) { return handleError(decodedError, 'cookie'); } - if (decodeResult.payload.iat < clientUat) { + if (decodeResult.payload.iat < authenticateContext.clientUat) { return handleMaybeHandshakeStatus(authenticateContext, AuthErrorReason.SessionTokenOutdated, ''); } try { - const { data, error } = await verifyToken(sessionToken!, authenticateContext); + const { data, error } = await verifyToken(authenticateContext.sessionTokenInCookie!, authenticateContext); if (error) { throw error; } @@ -359,40 +330,3 @@ export const debugRequestState = (params: RequestState) => { const { isSignedIn, proxyUrl, reason, message, publishableKey, isSatellite, domain } = params; return { isSignedIn, proxyUrl, reason, message, publishableKey, isSatellite, domain }; }; - -export type DebugRequestSate = ReturnType; - -/** - * Load authenticate request options related to headers. - */ -export const loadOptionsFromHeaders = (headers: ReturnType['headers']) => { - if (!headers) { - return {}; - } - - return { - sessionTokenInHeader: stripAuthorizationHeader(headers(constants.Headers.Authorization)), - origin: headers(constants.Headers.Origin), - host: headers(constants.Headers.Host), - forwardedHost: headers(constants.Headers.ForwardedHost), - forwardedProto: headers(constants.Headers.CloudFrontForwardedProto) || headers(constants.Headers.ForwardedProto), - referrer: headers(constants.Headers.Referrer), - userAgent: headers(constants.Headers.UserAgent), - secFetchDest: headers(constants.Headers.SecFetchDest), - accept: headers(constants.Headers.Accept), - }; -}; - -/** - * Load authenticate request options related to cookies. - */ -export const loadOptionsFromCookies = (cookies: ReturnType['cookies']) => { - if (!cookies) { - return {}; - } - - return { - sessionTokenInCookie: cookies?.(constants.Cookies.Session), - clientUat: cookies?.(constants.Cookies.ClientUat), - }; -}; diff --git a/packages/backend/src/tokens/types.ts b/packages/backend/src/tokens/types.ts new file mode 100644 index 0000000000..63be178410 --- /dev/null +++ b/packages/backend/src/tokens/types.ts @@ -0,0 +1,12 @@ +import type { VerifyTokenOptions } from './verify'; + +export type AuthenticateRequestOptions = { + publishableKey?: string; + domain?: string; + isSatellite?: boolean; + proxyUrl?: string; + signInUrl?: string; + signUpUrl?: string; + afterSignInUrl?: string; + afterSignUpUrl?: string; +} & VerifyTokenOptions; diff --git a/packages/backend/src/tokens/verify.ts b/packages/backend/src/tokens/verify.ts index 7d76a0a221..099b3c8737 100644 --- a/packages/backend/src/tokens/verify.ts +++ b/packages/backend/src/tokens/verify.ts @@ -7,29 +7,13 @@ import type { JwtReturnType } from '../jwt/types'; import type { LoadClerkJWKFromRemoteOptions } from './keys'; import { loadClerkJWKFromLocal, loadClerkJWKFromRemote } from './keys'; -/** - * - */ -export type VerifyTokenOptions = Pick & { - jwtKey?: string; -} & Pick; +export type VerifyTokenOptions = Omit & + Omit & { jwtKey?: string }; export async function verifyToken( token: string, options: VerifyTokenOptions, ): Promise> { - const { - secretKey, - apiUrl, - apiVersion, - audience, - authorizedParties, - clockSkewInMs, - jwksCacheTtlInMs, - jwtKey, - skipJwksCache, - } = options; - const { data: decodedResult, error: decodedError } = decodeJwt(token); if (decodedError) { return { error: decodedError }; @@ -41,11 +25,11 @@ export async function verifyToken( try { let key; - if (jwtKey) { - key = loadClerkJWKFromLocal(jwtKey); - } else if (secretKey) { + if (options.jwtKey) { + key = loadClerkJWKFromLocal(options.jwtKey); + } else if (options.secretKey) { // Fetch JWKS from Backend API using the key - key = await loadClerkJWKFromRemote({ secretKey, apiUrl, apiVersion, kid, jwksCacheTtlInMs, skipJwksCache }); + key = await loadClerkJWKFromRemote({ ...options, kid }); } else { return { error: new TokenVerificationError({ @@ -56,12 +40,7 @@ export async function verifyToken( }; } - return await verifyJwt(token, { - audience, - authorizedParties, - clockSkewInMs, - key, - }); + return await verifyJwt(token, { ...options, key }); } catch (error) { return { error: error as TokenVerificationError }; } diff --git a/packages/backend/src/util/IsomorphicRequest.ts b/packages/backend/src/util/IsomorphicRequest.ts deleted file mode 100644 index 3a2b8fdee6..0000000000 --- a/packages/backend/src/util/IsomorphicRequest.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { parse } from 'cookie'; - -import runtime from '../runtime'; -import { buildRequestUrl } from '../utils'; - -type IsomorphicRequestOptions = (Request: typeof runtime.Request, Headers: typeof runtime.Headers) => Request; -/** - * @internal - */ -export const createIsomorphicRequest = (cb: IsomorphicRequestOptions): Request => { - const req = cb(runtime.Request, runtime.Headers); - // Used to fix request.url using the x-forwarded-* headers - const headersGeneratedURL = buildRequestUrl(req); - return new runtime.Request(headersGeneratedURL, req); -}; - -export const buildRequest = (req: Request) => { - const cookies = parseIsomorphicRequestCookies(req); - const headers = getHeaderFromIsomorphicRequest(req); - const searchParams = getSearchParamsFromIsomorphicRequest(req); - const derivedRequestUrl = buildRequestUrl(req); - - return { - cookies, - headers, - searchParams, - derivedRequestUrl, - }; -}; - -const decode = (str: string): string => { - if (!str) { - return str; - } - return str.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent); -}; - -const parseIsomorphicRequestCookies = (req: Request) => { - const cookies = req.headers && req.headers?.get('cookie') ? parse(req.headers.get('cookie') || '') : {}; - return (key: string): string | undefined => { - const value = cookies?.[key]; - if (value === undefined) { - return undefined; - } - 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 ', ''); -}; diff --git a/packages/backend/src/util/__tests__/request.test.ts b/packages/backend/src/util/__tests__/request.test.ts deleted file mode 100644 index 8ef856e58a..0000000000 --- a/packages/backend/src/util/__tests__/request.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type QUnit from 'qunit'; - -import { checkCrossOrigin } from '../request'; - -export default (QUnit: QUnit) => { - const { module, test } = QUnit; - - module('check cross-origin-referrer request utility', () => { - test('is not CO with IPv6', assert => { - const originURL = new URL('http://[::1]'); - const host = new URL('http://[::1]').host; - assert.false(checkCrossOrigin({ originURL, host })); - }); - - test('is not CO with set https and 443 port', assert => { - const originURL = new URL('https://localhost:443'); - const host = new URL('https://localhost').host; - assert.false(checkCrossOrigin({ originURL, host })); - }); - - test('is CO with mixed default security ports', assert => { - const originURL = new URL('https://localhost:80'); - const host = new URL('http://localhost:443').host; - assert.true(checkCrossOrigin({ originURL, host })); - }); - - // todo( - // 'we cannot detect if the request is CO when HTTPS to HTTP and no other information is carried over', - // assert => { - // assert.true(true); - // }, - // ); - - test('is CO when HTTPS to HTTP with present x-forwarded-proto', assert => { - const originURL = new URL('https://localhost'); - const host = new URL('http://someserver').host; - const forwardedHost = new URL('http://localhost').host; - const forwardedProto = 'http'; - - assert.true(checkCrossOrigin({ originURL, host, forwardedHost, forwardedProto })); - }); - - test('is CO when HTTPS to HTTP with forwarded proto', assert => { - const originURL = new URL('https://localhost'); - const host = new URL('http://localhost').host; - const forwardedProto = 'http'; - - assert.true(checkCrossOrigin({ originURL, host, forwardedProto })); - }); - - test('is CO with cross origin auth domain', assert => { - const originURL = new URL('https://accounts.clerk.com'); - const host = new URL('https://localhost').host; - assert.true(checkCrossOrigin({ originURL, host })); - }); - - test('is CO when forwarded port overrides host derived port', assert => { - const originURL = new URL('https://localhost:443'); - const host = new URL('https://localhost:3001').host; - assert.true(checkCrossOrigin({ originURL, host })); - }); - - test('is not CO with port included in x-forwarded host', assert => { - /* Example https://www.rfc-editor.org/rfc/rfc7239#section-4 */ - const originURL = new URL('http://localhost:3000'); - const host = '127.0.0.1:3000'; - const forwardedHost = 'localhost:3000'; - assert.false(checkCrossOrigin({ originURL, host, forwardedHost })); - }); - - test('is CO with port included in x-forwarded host', assert => { - /* Example https://www.rfc-editor.org/rfc/rfc7239#section-4 */ - const originURL = new URL('http://localhost:3000'); - const host = '127.0.0.1:3000'; - const forwardedHost = 'localhost:4000'; - assert.true(checkCrossOrigin({ originURL, host, forwardedHost })); - }); - - test('is not CO when forwarded port and origin does not contain a port - http', assert => { - const originURL = new URL('http://localhost'); - const host = new URL('http://localhost').host; - - assert.false(checkCrossOrigin({ originURL, host })); - }); - - test('is not CO when forwarded port and origin does not contain a port - https', assert => { - const originURL = new URL('https://localhost'); - const host = new URL('https://localhost').host; - - assert.false(checkCrossOrigin({ originURL, host })); - }); - - test('is not CO based on referrer with forwarded host & port and referer', assert => { - const host = ''; - const forwardedHost = 'example.com'; - const referrer = 'http://example.com/'; - - assert.false(checkCrossOrigin({ originURL: new URL(referrer), host, forwardedHost })); - }); - - test('is not CO for AWS Amplify', assert => { - const options = { - originURL: new URL('https://main.d38v5rl8fqcx2i.amplifyapp.com'), - host: 'prod.eu-central-1.gateway.amplify.aws.dev', - forwardedPort: '443,80', - forwardedHost: 'main.d38v5rl8fqcx2i.amplifyapp.com', - forwardedProto: 'https,http', - }; - assert.false(checkCrossOrigin(options)); - }); - - test('is not CO for Railway App', assert => { - const options = { - originURL: new URL('https://aws-clerk-nextjs-production.up.railway.app'), - host: 'aws-clerk-nextjs-production.up.railway.app', - forwardedPort: '80', - forwardedHost: 'aws-clerk-nextjs-production.up.railway.app', - forwardedProto: 'https,http', - }; - assert.false(checkCrossOrigin(options)); - }); - - test('is not CO for localhost application running in non http port', assert => { - const options = { - originURL: new URL('http://localhost:4011/protected'), - host: 'localhost:4011', - forwardedHost: 'localhost:4011', - forwardedPort: '4011', - forwardedProto: 'http', - }; - - assert.false(checkCrossOrigin(options)); - }); - }); -}; diff --git a/packages/backend/src/util/request.ts b/packages/backend/src/util/request.ts deleted file mode 100644 index b719836c61..0000000000 --- a/packages/backend/src/util/request.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { buildOrigin } from '../utils'; -/** - * This function is only used in the case where: - * - DevOrStaging key is present - * - The request carries referrer information - * (This case most of the times signifies redirect from Clerk Auth pages) - * - */ -export function checkCrossOrigin({ - originURL, - host, - forwardedHost, - forwardedProto, -}: { - originURL: URL; - host?: string | null; - forwardedHost?: string | null; - forwardedProto?: string | null; -}) { - const finalURL = buildOrigin({ forwardedProto, forwardedHost, protocol: originURL.protocol, host }); - return finalURL && new URL(finalURL).origin !== originURL.origin; -} - -export function convertHostHeaderValueToURL(host?: string, protocol = 'https'): URL { - /** - * The protocol is added for the URL constructor to work properly. - * We do not check for the protocol at any point later on. - */ - return new URL(`${protocol}://${host}`); -} - -type ErrorFields = { - message: string; - long_message: string; - code: string; -}; - -export const getErrorObjectByCode = (errors: ErrorFields[], code: string) => { - if (!errors) { - return null; - } - - return errors.find((err: ErrorFields) => err.code === code); -}; diff --git a/packages/backend/src/utils.ts b/packages/backend/src/utils.ts deleted file mode 100644 index 9482519394..0000000000 --- a/packages/backend/src/utils.ts +++ /dev/null @@ -1,43 +0,0 @@ -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) => URL; -/** - * @internal - */ -export const buildRequestUrl: BuildRequestUrl = request => { - const initialUrl = new URL(request.url); - - 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 }); - - return new URL(initialUrl.pathname + initialUrl.search, base); -}; - -type BuildOriginParams = { - protocol?: string; - forwardedProto?: string | null; - forwardedHost?: string | null; - host?: string | null; -}; -type BuildOrigin = (params: BuildOriginParams) => string; -/** - * @internal - */ -export const buildOrigin: BuildOrigin = ({ protocol, forwardedProto, forwardedHost, host }) => { - const resolvedHost = getFirstValueFromHeader(forwardedHost) ?? host; - const resolvedProtocol = getFirstValueFromHeader(forwardedProto) ?? protocol?.replace(/[:/]/, ''); - - if (!resolvedHost || !resolvedProtocol) { - return ''; - } - - return `${resolvedProtocol}://${resolvedHost}`; -}; diff --git a/packages/backend/tests/suites.ts b/packages/backend/tests/suites.ts index cbb96ac9db..b896793858 100644 --- a/packages/backend/tests/suites.ts +++ b/packages/backend/tests/suites.ts @@ -3,19 +3,18 @@ import exportsTest from './dist/__tests__/exports.test.js'; import redirectTest from './dist/__tests__/redirections.test.js'; -import utilsTest from './dist/__tests__/utils.test.js'; import factoryTest from './dist/api/__tests__/factory.test.js'; import jwtAssertionsTest from './dist/jwt/__tests__/assertions.test.js'; import cryptoKeysTest from './dist/jwt/__tests__/cryptoKeys.test.js'; import signJwtTest from './dist/jwt/__tests__/signJwt.test.js'; import verifyJwtTest from './dist/jwt/__tests__/verifyJwt.test.js'; import authObjectsTest from './dist/tokens/__tests__/authObjects.test.js'; +import clerkRequestTest from './dist/tokens/__tests__/clerkRequest.test.js'; import tokenFactoryTest from './dist/tokens/__tests__/factory.test.js'; import keysTest from './dist/tokens/__tests__/keys.test.js'; import requestTest from './dist/tokens/__tests__/request.test.js'; import verifyTest from './dist/tokens/__tests__/verify.test.js'; import pathTest from './dist/util/__tests__/path.test.js'; -import utilRequestTest from './dist/util/__tests__/request.test.js'; // Add them to the suite array const suites = [ @@ -30,10 +29,9 @@ const suites = [ requestTest, signJwtTest, tokenFactoryTest, - utilRequestTest, - utilsTest, verifyJwtTest, verifyTest, + clerkRequestTest, ]; export default suites; diff --git a/packages/eslint-config-custom/typescript.js b/packages/eslint-config-custom/typescript.js index dc1082650b..9dc2d58db7 100644 --- a/packages/eslint-config-custom/typescript.js +++ b/packages/eslint-config-custom/typescript.js @@ -22,6 +22,7 @@ const disabledRules = { '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-unsafe-return': 'off', '@typescript-eslint/restrict-template-expressions': 'off', + '@typescript-eslint/no-unsafe-declaration-merging': 'off', // TODO: All rules below should be set to their defaults // when we're able to make the appropriate changes. diff --git a/packages/fastify/src/utils.ts b/packages/fastify/src/utils.ts index cdc923b472..fe1ecc1d32 100644 --- a/packages/fastify/src/utils.ts +++ b/packages/fastify/src/utils.ts @@ -19,7 +19,7 @@ export const fastifyRequestToRequest = (req: FastifyRequest): Request => { // Making some manual tests it seems that FastifyRequest populates the req protocol / hostname // based on the forwarded headers. Nevertheless, we are gonna use a dummy base and the request - // will be fixed by the createIsomorphicRequest. + // will be fixed by the internals of the clerk/backend package const dummyOriginReqUrl = new URL(req.url || '', `${req.protocol}://clerk-dummy`); return new Request(dummyOriginReqUrl, { method: req.method, headers }); }; diff --git a/packages/nextjs/src/server/authMiddleware.test.ts b/packages/nextjs/src/server/authMiddleware.test.ts index 1e6559c654..e0c06285d9 100644 --- a/packages/nextjs/src/server/authMiddleware.test.ts +++ b/packages/nextjs/src/server/authMiddleware.test.ts @@ -2,9 +2,8 @@ // This mock SHOULD exist before the import of authenticateRequest import { AuthStatus } from '@clerk/backend/internal'; import { expectTypeOf } from 'expect-type'; -import { NextURL } from 'next/dist/server/web/next-url'; -import type { NextFetchEvent, NextRequest } from 'next/server'; -import { NextResponse } from 'next/server'; +import type { NextFetchEvent } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server'; const authenticateRequestMock = jest.fn().mockResolvedValue({ toAuth: () => ({}), @@ -72,15 +71,14 @@ const mockRequest = ({ method = 'GET', headers = new Headers(), }: MockRequestParams) => { - return { - url: new URL(url, 'https://www.clerk.com').toString(), - nextUrl: new NextURL(url, 'https://www.clerk.com'), - cookies: { - get: () => (appendDevBrowserCookie ? { name: '__clerk_db_jwt', value: 'test_jwt' } : {}) as any, - }, + const headersWithCookie = new Headers(headers); + if (appendDevBrowserCookie) { + headersWithCookie.append('cookie', '__clerk_db_jwt=test_jwt'); + } + return new NextRequest(new URL(url, 'https://www.clerk.com').toString(), { method, - headers, - } as NextRequest; + headers: headersWithCookie, + }); }; describe('isPublicRoute', () => { @@ -602,27 +600,6 @@ describe('Type tests', () => { it('domain + isSatellite (satellite app)', () => { expectTypeOf({ ...defaultProps, domain: 'test', isSatellite: true }).toMatchTypeOf(); }); - - it('only domain is not allowed', () => { - expectTypeOf({ ...defaultProps, domain: 'test' }).not.toMatchTypeOf(); - }); - - it('only isSatellite is not allowed', () => { - expectTypeOf({ ...defaultProps, isSatellite: true }).not.toMatchTypeOf(); - }); - - it('proxyUrl + domain is not allowed', () => { - expectTypeOf({ ...defaultProps, proxyUrl: 'test', domain: 'test' }).not.toMatchTypeOf(); - }); - - it('proxyUrl + domain + isSatellite is not allowed', () => { - expectTypeOf({ - ...defaultProps, - proxyUrl: 'test', - domain: 'test', - isSatellite: true, - }).not.toMatchTypeOf(); - }); }); }); }); diff --git a/packages/nextjs/src/server/authMiddleware.ts b/packages/nextjs/src/server/authMiddleware.ts index d29b71f22a..d0803606b6 100644 --- a/packages/nextjs/src/server/authMiddleware.ts +++ b/packages/nextjs/src/server/authMiddleware.ts @@ -1,5 +1,5 @@ -import type { AuthenticateRequestOptions, AuthObject } from '@clerk/backend/internal'; -import { AuthStatus, buildRequestUrl, constants } from '@clerk/backend/internal'; +import type { AuthenticateRequestOptions, AuthObject, ClerkRequest } from '@clerk/backend/internal'; +import { AuthStatus, constants, createClerkRequest } from '@clerk/backend/internal'; import { DEV_BROWSER_JWT_KEY, setDevBrowserJWTInURL } from '@clerk/shared/devBrowser'; import { isDevelopmentFromSecretKey } from '@clerk/shared/keys'; import { eventMethodCalled } from '@clerk/shared/telemetry'; @@ -14,7 +14,7 @@ import { clerkClient } from './clerkClient'; import { PUBLISHABLE_KEY, SECRET_KEY } from './constants'; import { informAboutProtectedRouteInfo, receivedRequestForIgnoredRoute } from './errors'; import { redirectToSignIn } from './redirect'; -import type { NextMiddlewareResult, WithAuthOptions } from './types'; +import type { NextMiddlewareResult } from './types'; import { apiEndpointUnauthorizedNextResponse, decorateRequest, @@ -83,7 +83,7 @@ type AfterAuthHandler = ( evt: NextFetchEvent, ) => NextMiddlewareResult | Promise; -type AuthMiddlewareParams = WithAuthOptions & { +type AuthMiddlewareParams = AuthenticateRequestOptions & { /** * A function that is called before the authentication middleware is executed. * If a redirect response is returned, the middleware will respect it and redirect the user. @@ -157,28 +157,32 @@ const authMiddleware: AuthMiddleware = (...args: unknown[]) => { if (options.debug) { logger.enable(); } - const req = withNormalizedClerkUrl(_req); + const clerkRequest = createClerkRequest(_req); + const nextRequest = withNormalizedClerkUrl(clerkRequest, _req); logger.debug('URL debug', { - url: req.nextUrl.href, - method: req.method, - headers: stringifyHeaders(req.headers), - nextUrl: req.nextUrl.href, - clerkUrl: req.experimental_clerkUrl.href, + url: nextRequest.nextUrl.href, + method: nextRequest.method, + headers: stringifyHeaders(nextRequest.headers), + nextUrl: nextRequest.nextUrl.href, + clerkUrl: nextRequest.experimental_clerkUrl.href, }); logger.debug('Options debug', { ...options, beforeAuth: !!beforeAuth, afterAuth: !!afterAuth }); - if (isIgnoredRoute(req)) { + if (isIgnoredRoute(nextRequest)) { logger.debug({ isIgnoredRoute: true }); if (isDevelopmentFromSecretKey(options.secretKey || SECRET_KEY) && !params.ignoredRoutes) { console.warn( - receivedRequestForIgnoredRoute(req.experimental_clerkUrl.href, JSON.stringify(DEFAULT_CONFIG_MATCHER)), + receivedRequestForIgnoredRoute( + nextRequest.experimental_clerkUrl.href, + JSON.stringify(DEFAULT_CONFIG_MATCHER), + ), ); } return setHeader(NextResponse.next(), constants.Headers.AuthReason, 'ignored-route'); } - const beforeAuthRes = await (beforeAuth && beforeAuth(req, evt)); + const beforeAuthRes = await (beforeAuth && beforeAuth(nextRequest, evt)); if (beforeAuthRes === false) { logger.debug('Before auth returned false, skipping'); @@ -193,9 +197,9 @@ const authMiddleware: AuthMiddleware = (...args: unknown[]) => { ...options, secretKey: options.secretKey || SECRET_KEY, publishableKey: options.publishableKey || PUBLISHABLE_KEY, - ...handleMultiDomainAndProxy(req, options as AuthenticateRequestOptions), + ...handleMultiDomainAndProxy(clerkRequest, options as AuthenticateRequestOptions), } as AuthenticateRequestOptions; - const requestState = await clerkClient.authenticateRequest(req, authenticateRequestOptions); + const requestState = await clerkClient.authenticateRequest(clerkRequest, authenticateRequestOptions); const locationHeader = requestState.headers.get('location'); if (locationHeader) { @@ -211,26 +215,26 @@ const authMiddleware: AuthMiddleware = (...args: unknown[]) => { } const auth = Object.assign(requestState.toAuth(), { - isPublicRoute: isPublicRoute(req), - isApiRoute: isApiRoute(req), + isPublicRoute: isPublicRoute(nextRequest), + isApiRoute: isApiRoute(nextRequest), }); logger.debug(() => ({ auth: JSON.stringify(auth), debug: auth.debug() })); - const afterAuthRes = await (afterAuth || defaultAfterAuth)(auth, req, evt); + const afterAuthRes = await (afterAuth || defaultAfterAuth)(auth, nextRequest, evt); const finalRes = mergeResponses(beforeAuthRes, afterAuthRes) || NextResponse.next(); logger.debug(() => ({ mergedHeaders: stringifyHeaders(finalRes.headers) })); if (isRedirect(finalRes)) { logger.debug('Final response is redirect, following redirect'); const res = setHeader(finalRes, constants.Headers.AuthReason, 'redirect'); - return appendDevBrowserOnCrossOrigin(req, res, options); + return appendDevBrowserOnCrossOrigin(nextRequest, res, options); } if (options.debug) { - setRequestHeadersOnNextResponse(finalRes, req, { [constants.Headers.EnableDebug]: 'true' }); + setRequestHeadersOnNextResponse(finalRes, nextRequest, { [constants.Headers.EnableDebug]: 'true' }); logger.debug(`Added ${constants.Headers.EnableDebug} on request`); } - const result = decorateRequest(req, finalRes, requestState) || NextResponse.next(); + const result = decorateRequest(nextRequest, finalRes, requestState) || NextResponse.next(); if (requestState.headers) { requestState.headers.forEach((value, key) => { @@ -360,16 +364,12 @@ const isRequestMethodIndicatingApiRoute = (req: NextRequest): boolean => { return !['get', 'head', 'options'].includes(requestMethod); }; -const withNormalizedClerkUrl = (req: NextRequest): WithClerkUrl => { - const clerkUrl = req.nextUrl.clone(); - - const originUrl = buildRequestUrl(req); - - clerkUrl.port = originUrl.port; - clerkUrl.protocol = originUrl.protocol; - clerkUrl.host = originUrl.host; - - return Object.assign(req, { experimental_clerkUrl: clerkUrl }); +const withNormalizedClerkUrl = (clerkRequest: ClerkRequest, nextRequest: NextRequest): WithClerkUrl => { + const res = nextRequest.nextUrl.clone(); + res.port = clerkRequest.clerkUrl.port; + res.protocol = clerkRequest.clerkUrl.protocol; + res.host = clerkRequest.clerkUrl.host; + return Object.assign(nextRequest, { experimental_clerkUrl: res }); }; const informAboutProtectedRoute = (path: string, params: AuthMiddlewareParams, isApiRoute: boolean) => { diff --git a/packages/nextjs/src/server/getAuth.ts b/packages/nextjs/src/server/getAuth.ts index f9b3435e65..df60953696 100644 --- a/packages/nextjs/src/server/getAuth.ts +++ b/packages/nextjs/src/server/getAuth.ts @@ -58,7 +58,8 @@ export const createGetAuth = ({ const jwt = parseJwt(req); logger.debug('JWT debug', jwt.raw.text); - return signedInAuthObject(jwt.payload, { ...options, token: jwt.raw.text }); + // @ts-expect-error - TODO @nikos: Align types + return signedInAuthObject({ ...options, sessionToken: jwt.raw.text }, jwt.payload); }; }); @@ -106,7 +107,8 @@ export const buildClerkProps: BuildClerkProps = (req, initState = {}) => { authObject = signedOutAuthObject(options); } else { const { payload, raw } = parseJwt(req); - authObject = signedInAuthObject(payload, { ...options, token: raw.text }); + // @ts-expect-error - TODO @nikos: Align types + authObject = signedInAuthObject({ ...options, sessionToken: raw.text }, payload); } const sanitizedAuthObject = makeAuthObjectSerializable(stripPrivateDataFromObject({ ...authObject, ...initState })); diff --git a/packages/nextjs/src/server/types.ts b/packages/nextjs/src/server/types.ts index 71ff924343..bb395c543e 100644 --- a/packages/nextjs/src/server/types.ts +++ b/packages/nextjs/src/server/types.ts @@ -1,5 +1,3 @@ -import type { OptionalVerifyTokenOptions } from '@clerk/backend/internal'; -import type { MultiDomainAndOrProxy } from '@clerk/types'; import type { IncomingMessage } from 'http'; import type { NextApiRequest } from 'next'; import type { NextApiRequestCookies } from 'next/dist/server/api-utils'; @@ -12,11 +10,4 @@ type GsspRequest = IncomingMessage & { export type RequestLike = NextRequest | NextApiRequest | GsspRequest; -export type WithAuthOptions = OptionalVerifyTokenOptions & - MultiDomainAndOrProxy & { - publishableKey?: string; - secretKey?: string; - signInUrl?: string; - }; - export type NextMiddlewareResult = Awaited>; diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 635c9f435a..1d04efce74 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -1,5 +1,5 @@ -import type { AuthenticateRequestOptions, RequestState } from '@clerk/backend/internal'; -import { buildRequestUrl, constants } from '@clerk/backend/internal'; +import type { AuthenticateRequestOptions, ClerkRequest, RequestState } from '@clerk/backend/internal'; +import { constants } from '@clerk/backend/internal'; import { handleValueOrFn } from '@clerk/shared/handleValueOrFn'; import { isDevelopmentFromSecretKey } from '@clerk/shared/keys'; import { isHttpOrHttps } from '@clerk/shared/proxy'; @@ -9,7 +9,7 @@ import { NextResponse } from 'next/server'; import { constants as nextConstants } from '../constants'; import { DOMAIN, IS_SATELLITE, PROXY_URL, SECRET_KEY, SIGN_IN_URL } from './constants'; import { missingDomainAndProxy, missingSignInUrlInDev } from './errors'; -import type { NextMiddlewareResult, RequestLike } from './types'; +import type { RequestLike } from './types'; type AuthKey = 'AuthStatus' | 'AuthMessage' | 'AuthReason'; @@ -77,7 +77,7 @@ const MIDDLEWARE_HEADER_PREFIX = 'x-middleware-request' as string; export const setRequestHeadersOnNextResponse = ( res: NextResponse | Response, - req: NextRequest, + req: Request, newHeaders: Record, ) => { if (!res.headers.get(OVERRIDE_HEADERS)) { @@ -107,11 +107,7 @@ export const injectSSRStateIntoObject = (obj: O, authObject: T) => { }; // Auth result will be set as both a query param & header when applicable -export function decorateRequest( - req: NextRequest, - res: NextMiddlewareResult, - requestState: RequestState, -): NextMiddlewareResult { +export function decorateRequest(req: Request, res: Response, requestState: RequestState): Response { const { reason, message, status } = requestState; // pass-through case, convert to next() if (!res) { @@ -166,18 +162,18 @@ export const isCrossOrigin = (from: string | URL, to: string | URL) => { return fromUrl.origin !== toUrl.origin; }; -export const handleMultiDomainAndProxy = (req: NextRequest, opts: AuthenticateRequestOptions) => { - const requestURL = buildRequestUrl(req); - const relativeOrAbsoluteProxyUrl = handleValueOrFn(opts?.proxyUrl, requestURL, PROXY_URL); +export const handleMultiDomainAndProxy = (clerkRequest: ClerkRequest, opts: AuthenticateRequestOptions) => { + const relativeOrAbsoluteProxyUrl = handleValueOrFn(opts?.proxyUrl, clerkRequest.clerkUrl, PROXY_URL); + let proxyUrl; if (!!relativeOrAbsoluteProxyUrl && !isHttpOrHttps(relativeOrAbsoluteProxyUrl)) { - proxyUrl = new URL(relativeOrAbsoluteProxyUrl, requestURL).toString(); + proxyUrl = new URL(relativeOrAbsoluteProxyUrl, clerkRequest.clerkUrl).toString(); } else { proxyUrl = relativeOrAbsoluteProxyUrl; } - const isSatellite = handleValueOrFn(opts.isSatellite, new URL(req.url), IS_SATELLITE); - const domain = handleValueOrFn(opts.domain, new URL(req.url), DOMAIN); + const isSatellite = handleValueOrFn(opts.isSatellite, new URL(clerkRequest.url), IS_SATELLITE); + const domain = handleValueOrFn(opts.domain, new URL(clerkRequest.url), DOMAIN); const signInUrl = opts?.signInUrl || SIGN_IN_URL; if (isSatellite && !proxyUrl && !domain) { diff --git a/packages/remix/src/ssr/authenticateRequest.ts b/packages/remix/src/ssr/authenticateRequest.ts index 0058f2b7b4..509ef05b30 100644 --- a/packages/remix/src/ssr/authenticateRequest.ts +++ b/packages/remix/src/ssr/authenticateRequest.ts @@ -1,6 +1,6 @@ import { createClerkClient } from '@clerk/backend'; import type { SignedInState, SignedOutState } from '@clerk/backend/internal'; -import { AuthStatus, buildRequestUrl } from '@clerk/backend/internal'; +import { AuthStatus, createClerkRequest } from '@clerk/backend/internal'; import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey'; import { handleValueOrFn } from '@clerk/shared/handleValueOrFn'; import { isDevelopmentFromSecretKey } from '@clerk/shared/keys'; @@ -16,6 +16,7 @@ export async function authenticateRequest( opts: RootAuthLoaderOptions = {}, ): Promise { const { request, context } = args; + const clerkRequest = createClerkRequest(request); const { audience, authorizedParties } = opts; // Fetch environment variables across Remix runtime. @@ -42,17 +43,15 @@ export async function authenticateRequest( isTruthy(getEnvVariable('CLERK_IS_SATELLITE', context)) || false; - const requestURL = buildRequestUrl(request); - const relativeOrAbsoluteProxyUrl = handleValueOrFn( opts?.proxyUrl, - requestURL, + clerkRequest.clerkUrl, getEnvVariable('CLERK_PROXY_URL', context), ); let proxyUrl; if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) { - proxyUrl = new URL(relativeOrAbsoluteProxyUrl, requestURL).toString(); + proxyUrl = new URL(relativeOrAbsoluteProxyUrl, clerkRequest.clerkUrl).toString(); } else { proxyUrl = relativeOrAbsoluteProxyUrl; } diff --git a/packages/sdk-node/src/authenticateRequest.ts b/packages/sdk-node/src/authenticateRequest.ts index cc04de1097..04d346fb97 100644 --- a/packages/sdk-node/src/authenticateRequest.ts +++ b/packages/sdk-node/src/authenticateRequest.ts @@ -1,5 +1,5 @@ import type { RequestState } from '@clerk/backend/internal'; -import { buildRequestUrl, constants } from '@clerk/backend/internal'; +import { constants, createClerkRequest } from '@clerk/backend/internal'; import { handleValueOrFn } from '@clerk/shared/handleValueOrFn'; import { isDevelopmentFromSecretKey } from '@clerk/shared/keys'; import { isHttpOrHttps, isProxyUrlRelative, isValidProxyUrl } from '@clerk/shared/proxy'; @@ -12,15 +12,14 @@ export const authenticateRequest = (opts: AuthenticateRequestParams) => { const { clerkClient, secretKey, publishableKey, req: incomingMessage, options } = opts; const { jwtKey, authorizedParties, audience } = options || {}; - const req = incomingMessageToRequest(incomingMessage); + const clerkRequest = createClerkRequest(incomingMessageToRequest(incomingMessage)); const env = { ...loadApiEnv(), ...loadClientEnv() }; - const requestUrl = buildRequestUrl(req); - const isSatellite = handleValueOrFn(options?.isSatellite, requestUrl, env.isSatellite); - const domain = handleValueOrFn(options?.domain, requestUrl) || env.domain; + const isSatellite = handleValueOrFn(options?.isSatellite, clerkRequest.clerkUrl, env.isSatellite); + const domain = handleValueOrFn(options?.domain, clerkRequest.clerkUrl) || env.domain; const signInUrl = options?.signInUrl || env.signInUrl; const proxyUrl = absoluteProxyUrl( - handleValueOrFn(options?.proxyUrl, requestUrl, env.proxyUrl), - requestUrl.toString(), + handleValueOrFn(options?.proxyUrl, clerkRequest.clerkUrl, env.proxyUrl), + clerkRequest.clerkUrl.toString(), ); if (isSatellite && !proxyUrl && !domain) { @@ -31,7 +30,7 @@ export const authenticateRequest = (opts: AuthenticateRequestParams) => { throw new Error(satelliteAndMissingSignInUrl); } - return clerkClient.authenticateRequest(req, { + return clerkClient.authenticateRequest(clerkRequest, { audience, secretKey, publishableKey,