Skip to content

Commit

Permalink
feat(backend): Determine suffixed / un-suddixed cookies based on mult…
Browse files Browse the repository at this point in the history
…iple conditions [WIP]
  • Loading branch information
dimkl committed Jun 5, 2024
1 parent 5d57ade commit f12aaa0
Show file tree
Hide file tree
Showing 4 changed files with 325 additions and 2 deletions.
22 changes: 22 additions & 0 deletions packages/backend/src/fixtures/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { base64url } from '../util/rfc4648';

export const mockJwt =
'eyJhbGciOiJSUzI1NiIsImtpZCI6Imluc18yR0lvUWhiVXB5MGhYN0IyY1ZrdVRNaW5Yb0QiLCJ0eXAiOiJKV1QifQ.eyJhenAiOiJodHRwczovL2FjY291bnRzLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsImV4cCI6MTY2NjY0ODMxMCwiaWF0IjoxNjY2NjQ4MjUwLCJpc3MiOiJodHRwczovL2NsZXJrLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsIm5iZiI6MTY2NjY0ODI0MCwic2lkIjoic2Vzc18yR2JEQjRlbk5kQ2E1dlMxenBDM1h6Zzl0SzkiLCJzdWIiOiJ1c2VyXzJHSXBYT0VwVnlKdzUxcmtabjlLbW5jNlN4ciJ9.n1Usc-DLDftqA0Xb-_2w8IGs4yjCmwc5RngwbSRvwevuZOIuRoeHmE2sgCdEvjfJEa7ewL6EVGVcM557TWPW--g_J1XQPwBy8tXfz7-S73CEuyRFiR97L2AHRdvRtvGtwR-o6l8aHaFxtlmfWbQXfg4kFJz2UGe9afmh3U9-f_4JOZ5fa3mI98UMy1-bo20vjXeWQ9aGrqaxHQxjnzzC-1Kpi5LdPvhQ16H0dPB8MHRTSM5TAuLKTpPV7wqixmbtcc2-0k6b9FKYZNqRVTaIyV-lifZloBvdzlfOF8nW1VVH_fx-iW5Q3hovHFcJIULHEC1kcAYTubbxzpgeVQepGg';

Expand All @@ -7,6 +9,9 @@ export const mockInvalidSignatureJwt =
export const mockMalformedJwt =
'eyJhbGciOiJSUzI1NiIsImtpZCI6Imluc18yR0lvUWhiVXB5MGhYN0IyY1ZrdVRNaW5Yb0QiLCJ0eXAiOiJKV1QifQ.eyJpYXQiOjE2NjY2NDgyNTB9.n1Usc-DLDftqA0Xb-_2w8IGs4yjCmwc5RngwbSRvwevuZOIuRoeHmE2sgCdEvjfJEa7ewL6EVGVcM557TWPW--g_J1XQPwBy8tXfz7-S73CEuyRFiR97L2AHRdvRtvGtwR-o6l8aHaFxtlmfWbQXfg4kFJz2UGe9afmh3U9-f_4JOZ5fa3mI98UMy1-bo20vjXeWQ9aGrqaxHQxjnzzC-1Kpi5LdPvhQ16H0dPB8MHRTSM5TAuLKTpPV7wqixmbtcc2-0k6b9FKYZNqRVTaIyV-lifZloBvdzlfOF8nW1VVH_fx-iW5Q3hovHFcJIULHEC1kcAYTubbxzpgeVQepGg';

const mockJwtSignature =
'n1Usc-DLDftqA0Xb-_2w8IGs4yjCmwc5RngwbSRvwevuZOIuRoeHmE2sgCdEvjfJEa7ewL6EVGVcM557TWPW--g_J1XQPwBy8tXfz7-S73CEuyRFiR97L2AHRdvRtvGtwR-o6l8aHaFxtlmfWbQXfg4kFJz2UGe9afmh3U9-f_4JOZ5fa3mI98UMy1-bo20vjXeWQ9aGrqaxHQxjnzzC-1Kpi5LdPvhQ16H0dPB8MHRTSM5TAuLKTpPV7wqixmbtcc2-0k6b9FKYZNqRVTaIyV-lifZloBvdzlfOF8nW1VVH_fx-iW5Q3hovHFcJIULHEC1kcAYTubbxzpgeVQepGg';

export const mockJwtHeader = {
alg: 'RS256',
kid: 'ins_2GIoQhbUpy0hX7B2cVkuTMinXoD',
Expand Down Expand Up @@ -137,3 +142,20 @@ export const publicJwks = {
// this jwt has be signed with the keys above. The payload is mockJwtPayload and the header is mockJwtHeader
export const signedJwt =
'eyJhbGciOiJSUzI1NiIsImtpZCI6Imluc18yR0lvUWhiVXB5MGhYN0IyY1ZrdVRNaW5Yb0QiLCJ0eXAiOiJKV1QifQ.eyJhenAiOiJodHRwczovL2FjY291bnRzLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsImV4cCI6MTY2NjY0ODMxMCwiaWF0IjoxNjY2NjQ4MjUwLCJpc3MiOiJodHRwczovL2NsZXJrLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsIm5iZiI6MTY2NjY0ODI0MCwic2lkIjoic2Vzc18yR2JEQjRlbk5kQ2E1dlMxenBDM1h6Zzl0SzkiLCJzdWIiOiJ1c2VyXzJHSXBYT0VwVnlKdzUxcmtabjlLbW5jNlN4ciJ9.j3rB92k32WqbQDkFB093H4GoQsBVLH4HLGF6ObcwUaVGiHC8SEu6T31FuPf257SL8A5sSGtWWM1fqhQpdLohgZb_hbJswGBuYI-Clxl9BtpIRHbWFZkLBIj8yS9W9aVtD3fWBbF6PHx7BY1udio-rbGWg1YAOZNtVcxF02p-MvX-8XIK92Vwu3Un5zyfCoVIg__qo3Xntzw3tznsZ4XDe212c6kVz1R_L1d5DKjeWXpjUPAS_zFeZSIJEQLf4JNr4JCY38tfdnc3ajfDA3p36saf1XwmTdWXQKCXi75c2TJAXROs3Pgqr5Kw_5clygoFuxN5OEMhFWFSnvIBdi3M6w';

export const pkTest = 'pk_test_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA';
export const pkLive = 'pk_live_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA';

type CreateJwt = (opts?: { header?: any; payload?: any; signature?: string }) => string;
export const createJwt: CreateJwt = ({ header, payload, signature = mockJwtSignature } = {}) => {
const encoder = new TextEncoder();

const stringifiedHeader = JSON.stringify({ ...mockJwtHeader, ...header });
const stringifiedPayload = JSON.stringify({ ...mockJwtPayload, ...payload });

return [
base64url.stringify(encoder.encode(stringifiedHeader), { pad: false }),
base64url.stringify(encoder.encode(stringifiedPayload), { pad: false }),
signature,
].join('.');
};
238 changes: 238 additions & 0 deletions packages/backend/src/tokens/__tests__/authenticateContext.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import type QUnit from 'qunit';
import sinon from 'sinon';

import { createJwt, mockJwtPayload, pkLive, pkTest } from '../../fixtures';
import { createAuthenticateContext } from '../authenticateContext';
import { createClerkRequest } from '../clerkRequest';

function createCookieHeader(cookies: Record<string, string>): string {
return Object.keys(cookies)
.reduce((result: string[], cookieName: string) => {
return [...result, `${cookieName}=${cookies[cookieName]}`];
}, [])
.join('; ');
}
/**
* https://many-ghoul-34.accounts.lclclerk.com/sign-in?redirect_url=http%3A%2F%2Flocalhost%3A3001%2Fuser
* - [x] backend SDK with suffixed cookies & ClerkJS with suffixed cookies
* - [x] backend SDK with suffixed cookies & ClerkJS with un-suffixed cookies
* - [x] backend SDK with suffixed cookies & downgrade from ClerkJS with suffixed to un-suffixed cookies
* - [x] multiple apps for the same instance deployed in subdomains (eg [foo.example.com](http://foo.example.com/) and [bar.example.com](http://bar.example.com/) with pkA/skA)
* - [x] multiple apps for the same development instance in localhost (eg localhost:3001 and localhost:3002 with pkA/skA)
* - [x] backend SDK with suffixed cookies & ClerkJS with un-suffixed cookies and sign-in from AP of development instance (devBrowser is passed)
*
* - multiple apps for the same instance deployed in subdomains (eg [foo.example.com](http://foo.example.com/) and [bar.example.com](http://bar.example.com/) with pkA/skA) with multi-session app
*/
export default (QUnit: QUnit) => {
const { module, test } = QUnit;

module('AuthenticateContext', hooks => {
let fakeClock;

const nowTimestampInSec = mockJwtPayload.iat;

const suffixedSession = createJwt({ header: {} });
const session = createJwt();
const sessionWithInvalidIssuer = createJwt({ payload: { iss: 'http:whatever' } });
const newSession = createJwt({ payload: { iat: nowTimestampInSec + 60 } });
const clientUat = '1717490192';
const suffixedClientUat = '1717490193';

hooks.beforeEach(() => {
fakeClock = sinon.useFakeTimers(nowTimestampInSec * 1000);
});

hooks.afterEach(() => {
fakeClock.restore();
sinon.restore();
});
module('suffixedCookies', () => {
module('use un-suffixed cookies', () => {
test('request with un-suffixed cookies', assert => {
const headers = new Headers({
cookie: createCookieHeader({
__client_uat: clientUat,
__session: session,
}),
});
const clerkRequest = createClerkRequest(new Request('http://example.com', { headers }));
const context = createAuthenticateContext(clerkRequest, {
publishableKey: pkLive,
});

assert.false(context.suffixedCookies);
assert.equal(context.sessionTokenInCookie, session);
assert.equal(context.clientUat, clientUat);
});

test('request with suffixed and valid newer un-suffixed cookies - case of ClerkJS downgrade', assert => {
const headers = new Headers({
cookie: createCookieHeader({
__client_uat: clientUat,
__client_uat_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA: suffixedClientUat,
__session: newSession,
__session_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA: suffixedSession,
__clerk_db_jwt: '__clerk_db_jwt',
__clerk_db_jwt_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA: '__clerk_db_jwt-suffixed',
}),
});
const clerkRequest = createClerkRequest(new Request('http://example.com', { headers }));
const context = createAuthenticateContext(clerkRequest, {
publishableKey: pkLive,
});

assert.false(context.suffixedCookies);
assert.equal(context.sessionTokenInCookie, newSession);
assert.equal(context.clientUat, clientUat);
assert.equal(context.devBrowserToken, '__clerk_db_jwt');
});

test('request with suffixed client_uat as signed-out and un-suffixed client_uat as signed-in - case of ClerkJS downgrade', assert => {
const headers = new Headers({
cookie: createCookieHeader({
__client_uat: clientUat,
__client_uat_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA: '0',
__session: session,
__clerk_db_jwt: '__clerk_db_jwt',
}),
});
const clerkRequest = createClerkRequest(new Request('http://example.com', { headers }));
const context = createAuthenticateContext(clerkRequest, {
publishableKey: pkTest,
});

assert.false(context.suffixedCookies);
assert.equal(context.sessionTokenInCookie, session);
assert.equal(context.clientUat, clientUat);
});

test('prod: request with suffixed session and signed-out suffixed client_uat', assert => {
const headers = new Headers({
cookie: createCookieHeader({
__client_uat: '0',
__client_uat_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA: '0',
__session: session,
__session_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA: suffixedSession,
}),
});
const clerkRequest = createClerkRequest(new Request('http://example.com', { headers }));
const context = createAuthenticateContext(clerkRequest, {
publishableKey: pkLive,
});

assert.false(context.suffixedCookies);
assert.equal(context.sessionTokenInCookie, session);
assert.equal(context.clientUat, '0');
});
});

module('use suffixed cookies', () => {
test('prod: request with valid suffixed and un-suffixed cookies', assert => {
const headers = new Headers({
cookie: createCookieHeader({
__client_uat: clientUat,
__client_uat_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA: suffixedClientUat,
__session: session,
__session_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA: suffixedSession,
}),
});
const clerkRequest = createClerkRequest(new Request('http://example.com', { headers }));
const context = createAuthenticateContext(clerkRequest, {
publishableKey: pkLive,
});
assert.true(context.suffixedCookies);
assert.equal(context.sessionTokenInCookie, suffixedSession);
assert.equal(context.clientUat, suffixedClientUat);
});

test('prod: request with invalid issuer un-suffixed and valid suffixed cookies - case of multiple apps on same eTLD+1 domain', assert => {
const headers = new Headers({
cookie: createCookieHeader({
__client_uat: clientUat,
__client_uat_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA: suffixedClientUat,
__session: sessionWithInvalidIssuer,
__session_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA: suffixedSession,
}),
});
const clerkRequest = createClerkRequest(new Request('http://example.com', { headers }));
const context = createAuthenticateContext(clerkRequest, {
publishableKey: pkLive,
});
assert.true(context.suffixedCookies);
assert.equal(context.sessionTokenInCookie, suffixedSession);
assert.equal(context.clientUat, suffixedClientUat);
});

test('dev: request with invalid issuer un-suffixed and valid multiple suffixed cookies - case of multiple apps on localhost', assert => {
const blahSession = createJwt({ payload: { iss: 'http://blah' } });
const fooSession = createJwt({ payload: { iss: 'http://foo' } });
const headers = new Headers({
cookie: createCookieHeader({
__client_uat: '0',
__client_uat_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA: '0',
__client_uat_Y2xlcmsuYmxhaC5wdW1hLTc1LmxjbC5kZXYk: '1717490193',
__client_uat_Y2xlcmsuZm9vLnB1bWEtNzUubGNsLmRldiQ: '1717490194',
__session: session,
__session_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA: suffixedSession,
__session_Y2xlcmsuYmxhaC5wdW1hLTc1LmxjbC5kZXYk: blahSession,
__session_Y2xlcmsuZm9vLnB1bWEtNzUubGNsLmRldiQ: fooSession,
__clerk_db_jwt: '__clerk_db_jwt',
__clerk_db_jwt_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA: '__clerk_db_jwt-suffixed',
__clerk_db_jwt_Y2xlcmsuYmxhaC5wdW1hLTc1LmxjbC5kZXYk: '__clerk_db_jwt-suffixed-blah',
__clerk_db_jwt_Y2xlcmsuZm9vLnB1bWEtNzUubGNsLmRldiQ: '__clerk_db_jwt-suffixed-foo',
}),
});
const clerkRequest = createClerkRequest(new Request('http://example.com', { headers }));
const context = createAuthenticateContext(clerkRequest, {
publishableKey: pkTest,
});

assert.true(context.suffixedCookies);
assert.equal(context.sessionTokenInCookie, suffixedSession);
assert.equal(context.clientUat, '0');
assert.equal(context.devBrowserToken, '__clerk_db_jwt-suffixed');
});

test('dev: request with suffixed session and signed-out suffixed client_uat', assert => {
const headers = new Headers({
cookie: createCookieHeader({
__client_uat: '0',
__client_uat_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA: '0',
__session: session,
__session_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA: suffixedSession,
__clerk_db_jwt: '__clerk_db_jwt',
__clerk_db_jwt_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA: '__clerk_db_jwt-suffixed',
}),
});
const clerkRequest = createClerkRequest(new Request('http://example.com', { headers }));
const context = createAuthenticateContext(clerkRequest, {
publishableKey: pkTest,
});

assert.true(context.suffixedCookies);
assert.equal(context.sessionTokenInCookie, suffixedSession);
assert.equal(context.clientUat, '0');
assert.equal(context.devBrowserToken, '__clerk_db_jwt-suffixed');
});

test('prod: request without suffixed session and signed-out suffixed client_uat', assert => {
const headers = new Headers({
cookie: createCookieHeader({
__client_uat: '0',
__client_uat_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA: '0',
__session: session,
}),
});
const clerkRequest = createClerkRequest(new Request('http://example.com', { headers }));
const context = createAuthenticateContext(clerkRequest, {
publishableKey: pkLive,
});

assert.true(context.suffixedCookies);
assert.strictEqual(context.sessionTokenInCookie, undefined);
assert.equal(context.clientUat, '0');
});
});
});
});
};
65 changes: 63 additions & 2 deletions packages/backend/src/tokens/authenticateContext.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { parsePublishableKey } from '@clerk/shared/keys';
import type { Jwt } from '@clerk/types';

import { constants } from '../constants';
import { decodeJwt } from '../jwt/verifyJwt';
import { assertValidPublishableKey } from '../util/optionsAssertions';
import type { ClerkRequest } from './clerkRequest';
import type { AuthenticateRequestOptions } from './types';
Expand Down Expand Up @@ -132,11 +134,70 @@ class AuthenticateContext {
return this.getCookie(cookieName);
}

private shouldUseSuffixed() {
private shouldUseSuffixed(): boolean {
const suffixedClientUat = this.getSuffixedCookie(constants.Cookies.ClientUat);
const clientUat = this.getCookie(constants.Cookies.ClientUat);
const suffixedSession = this.getSuffixedCookie(constants.Cookies.Session) || '';
const session = this.getCookie(constants.Cookies.Session) || '';

return suffixedClientUat === clientUat;
// If there is no suffixed cookies use un-suffixed
if (!suffixedClientUat && !suffixedSession) {
return false;
}

// If there's a token in un-suffixed, and it doesn't belong to this
// instance, then we must trust suffixed
if (session && !this.tokenBelongsToInstance(session)) {
return true;
}

const { data: sessionData } = decodeJwt(session);
const sessionIat = sessionData?.payload.iat || 0;
const { data: suffixedSessionData } = decodeJwt(suffixedSession);
const suffixedSessionIat = suffixedSessionData?.payload.iat || 0;

// Both indicate signed in, but un-suffixed is newer
// Trust un-suffixed because it's newer
if (suffixedClientUat !== '0' && clientUat !== '0' && sessionIat > suffixedSessionIat) {
return false;
}

// Suffixed indicates signed out, but un-suffixed indicates signed in
// Trust un-suffixed because it gets set with both new and old clerk.js,
// so we can assume it's newer
if (suffixedClientUat === '0' && clientUat !== '0') {
return false;
}

// In production, we can trust suffixed_uat because we don't need clerk.js to set it
if (this.instanceType === 'production') {
if (suffixedSession && suffixedClientUat === '0') {
return false;
}
} else {
const isSuffixedSessionExpired = this.sessionExpired(suffixedSessionData);
if (suffixedClientUat !== '0' && clientUat === '0' && isSuffixedSessionExpired) {
return false;
}
}
return true;
}

private tokenBelongsToInstance(token: string): boolean {
if (!token) {
return false;
}

const { data, errors } = decodeJwt(token);
if (errors) {
return false;
}
const tokenIssuer = data.payload.iss.replace(/https?:\/\//gi, '');
return this.frontendApi === tokenIssuer;
}

private sessionExpired(jwt: Jwt | undefined): boolean {
return !!jwt && jwt?.payload.exp <= (Date.now() / 1000) >> 0;
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/backend/tests/suites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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 authenticateContextTest from './dist/tokens/__tests__/authenticateContext.test.js';
import authObjectsTest from './dist/tokens/__tests__/authObjects.test.js';
import authStatusTest from './dist/tokens/__tests__/authStatus.test.js';
import clerkRequestTest from './dist/tokens/__tests__/clerkRequest.test.js';
Expand All @@ -19,6 +20,7 @@ import pathTest from './dist/util/__tests__/path.test.js';

// Add them to the suite array
const suites = [
authenticateContextTest,
authObjectsTest,
authStatusTest,
cryptoKeysTest,
Expand Down

0 comments on commit f12aaa0

Please sign in to comment.