Skip to content

Commit

Permalink
feat(*): Introduce ClerkRequest and refactor clerk/backend types (#2389)
Browse files Browse the repository at this point in the history
* feat(backend): Drop unused checkCrossOrigin util

* feat(backend): Drop createIsomorphicRequest

* feat(backend): Replace buildRequestUrl with ClerkRequest

A class that extends the native Request class, adds cookies helpers and a normalised clerkUrl that is constructed by using the values found in req.headers so it is able to work reliably when the app is running behind a proxy server.

* feat(backend): Introduce AuthenticateContext

A class that collects all data required to authenticate a request

* feat(backend): Simplify authenticateRequest and types

* fix(backend): Fix tests

* fix(backend): Export createClerkRequest under /internal

* fix(nextjs): Use new backend utils

* fix(nextjs): Fix init of clerkRequest
  • Loading branch information
nikosdouvlis committed Dec 18, 2023
1 parent 392b68c commit ad7bc70
Show file tree
Hide file tree
Showing 31 changed files with 482 additions and 807 deletions.
2 changes: 2 additions & 0 deletions .changeset/kind-onions-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
1 change: 1 addition & 0 deletions packages/backend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
},
],
Expand Down
3 changes: 1 addition & 2 deletions packages/backend/src/__tests__/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
113 changes: 0 additions & 113 deletions packages/backend/src/__tests__/utils.test.ts

This file was deleted.

7 changes: 4 additions & 3 deletions packages/backend/src/internal.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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';
3 changes: 2 additions & 1 deletion packages/backend/src/jwt/verifyJwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,9 @@ export type VerifyJwtOptions = {

export async function verifyJwt(
token: string,
{ audience, authorizedParties, clockSkewInMs, key }: VerifyJwtOptions,
options: VerifyJwtOptions,
): Promise<JwtReturnType<JwtPayload, TokenVerificationError>> {
const { audience, authorizedParties, clockSkewInMs, key } = options;
const clockSkew = clockSkewInMs || DEFAULT_CLOCK_SKEW_IN_SECONDS;

const { data: decoded, error } = decodeJwt(token);
Expand Down
116 changes: 116 additions & 0 deletions packages/backend/src/tokens/__tests__/clerkRequest.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
};
31 changes: 2 additions & 29 deletions packages/backend/src/tokens/__tests__/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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',
});
});
});
};
Loading

0 comments on commit ad7bc70

Please sign in to comment.