Skip to content

Commit

Permalink
fix(backend): Fix issue with Cloudfront forwarded protocol (#1817)
Browse files Browse the repository at this point in the history
Initial implementation and discussion can be found in:
#1547

Co-authored-by: @flybayer
Co-authored-by: @BRKalow
  • Loading branch information
dimkl committed Oct 2, 2023
1 parent 3eab421 commit bb2ec93
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/friendly-pillows-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/backend': patch
---

Update `authenticateRequest()` to respect the `CloudFront-Forwarded-Proto` header when determining the correct `forwardedProto` value. This fixes an issue when Clerk is used in applications that are deployed behind AWS CloudFront, where previously all requests were treated as cross-origin.
1 change: 1 addition & 0 deletions packages/backend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const Headers = {
AuthMessage: 'x-clerk-auth-message',
EnableDebug: 'x-clerk-debug',
ClerkRedirectTo: 'x-clerk-redirect-to',
CloudFrontForwardedProto: 'cloudfront-forwarded-proto',
Authorization: 'authorization',
ForwardedPort: 'x-forwarded-port',
ForwardedProto: 'x-forwarded-proto',
Expand Down
53 changes: 52 additions & 1 deletion packages/backend/src/tokens/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { AuthErrorReason, type AuthReason, AuthStatus, type RequestState } from
import { TokenVerificationErrorReason } from './errors';
import { mockInvalidSignatureJwt, mockJwks, mockJwt, mockJwtPayload, mockMalformedJwt } from './fixtures';
import type { AuthenticateRequestOptions } from './request';
import { authenticateRequest } from './request';
import { authenticateRequest, loadOptionsFromHeaders } from './request';

function assertSignedOut(
assert,
Expand Down Expand Up @@ -566,4 +566,55 @@ export default (QUnit: QUnit) => {
assertSignedInToAuth(assert, requestState);
});
});

module('tokens.loadOptionsFromHeaders', () => {
const defaultOptions = {
headerToken: '',
origin: '',
host: '',
forwardedHost: '',
forwardedPort: '',
forwardedProto: '',
referrer: '',
userAgent: '',
};

test('returns options even if headers exist', async assert => {
const headers = key => (key === 'x-forwarded-proto' ? 'https' : '');
const options = { forwardedProto: 'http' };
assert.propEqual(loadOptionsFromHeaders(options, headers), {
...defaultOptions,
forwardedProto: 'http',
});
});

test('returns forwarded headers from headers', async assert => {
const headersData = { 'x-forwarded-proto': 'http', 'x-forwarded-port': '80', 'x-forwarded-host': 'example.com' };
const headers = key => headersData[key] || '';

assert.propEqual(loadOptionsFromHeaders({}, headers), {
...defaultOptions,
forwardedProto: 'http',
forwardedPort: '80',
forwardedHost: 'example.com',
});
});

test('returns Cloudfront forwarded proto from headers even if forwarded proto header exists', async 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.propEqual(loadOptionsFromHeaders({}, headers), {
...defaultOptions,
forwardedProto: 'https',
forwardedPort: '80',
forwardedHost: 'example.com',
});
});
});
};
35 changes: 27 additions & 8 deletions packages/backend/src/tokens/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,19 +122,12 @@ export async function authenticateRequest(options: AuthenticateRequestOptions):

options = {
...options,
...loadOptionsFromHeaders(options, headers),
frontendApi: parsePublishableKey(options.publishableKey)?.frontendApi || options.frontendApi,
apiUrl: options.apiUrl || API_URL,
apiVersion: options.apiVersion || API_VERSION,
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,
};

Expand Down Expand Up @@ -208,3 +201,29 @@ export const debugRequestState = (params: RequestState) => {
};

export type DebugRequestSate = ReturnType<typeof debugRequestState>;

/**
* Load authenticate request options from the options provided or fallback to headers.
*/
export const loadOptionsFromHeaders = (
options: AuthenticateRequestOptions,
headers: ReturnType<typeof buildRequest>['headers'],
) => {
if (!headers) {
return {};
}

return {
headerToken: stripAuthorizationHeader(options.headerToken || headers(constants.Headers.Authorization)),
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.CloudFrontForwardedProto) ||
headers(constants.Headers.ForwardedProto),
referrer: options.referrer || headers(constants.Headers.Referrer),
userAgent: options.userAgent || headers(constants.Headers.UserAgent),
};
};
2 changes: 1 addition & 1 deletion packages/backend/src/util/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export default (QUnit: QUnit) => {
assert.false(checkCrossOrigin({ originURL: new URL(referrer), host, forwardedHost }));
});

test('is not CO for AWS', assert => {
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',
Expand Down
1 change: 1 addition & 0 deletions packages/fastify/src/__snapshots__/constants.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ exports[`constants from environment variables 1`] = `
"AuthStatus": "x-clerk-auth-status",
"Authorization": "authorization",
"ClerkRedirectTo": "x-clerk-redirect-to",
"CloudFrontForwardedProto": "cloudfront-forwarded-proto",
"ContentType": "content-type",
"EnableDebug": "x-clerk-debug",
"ForwardedHost": "x-forwarded-host",
Expand Down

0 comments on commit bb2ec93

Please sign in to comment.