From fbae70b376be1ae36e74b0e8e52339f215a22ad4 Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Mon, 8 Apr 2024 19:04:46 +0300 Subject: [PATCH 1/8] chore(clerk-js): Move cookie related code to core/cookies --- packages/clerk-js/src/core/__tests__/clerk.test.ts | 2 +- packages/clerk-js/src/core/clerk.ts | 4 ++-- .../SessionCookiePoller.ts | 2 +- .../SessionCookieService.ts | 12 +++++++----- .../src/{utils => core}/cookies/clientUat.ts | 0 .../src/{utils => core}/cookies/devBrowser.ts | 0 .../clerk-js/src/{utils => core/cookies}/safeLock.ts | 0 .../clerk-js/src/{utils => core}/cookies/session.ts | 0 packages/clerk-js/src/core/devBrowser.ts | 2 +- packages/clerk-js/src/core/services/index.ts | 1 - packages/clerk-js/src/utils/index.ts | 1 - 11 files changed, 12 insertions(+), 12 deletions(-) rename packages/clerk-js/src/core/{services/authentication => cookies}/SessionCookiePoller.ts (94%) rename packages/clerk-js/src/core/{services/authentication => cookies}/SessionCookieService.ts (89%) rename packages/clerk-js/src/{utils => core}/cookies/clientUat.ts (100%) rename packages/clerk-js/src/{utils => core}/cookies/devBrowser.ts (100%) rename packages/clerk-js/src/{utils => core/cookies}/safeLock.ts (100%) rename packages/clerk-js/src/{utils => core}/cookies/session.ts (100%) delete mode 100644 packages/clerk-js/src/core/services/index.ts diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index 2401c532486..6788c14d1be 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -3,11 +3,11 @@ import { waitFor } from '@testing-library/dom'; import { mockNativeRuntime } from '../../testUtils'; import { Clerk } from '../clerk'; +import { SessionCookieService } from '../cookies/SessionCookieService'; import type { DevBrowser } from '../devBrowser'; import { eventBus, events } from '../events'; import type { DisplayConfig, Organization } from '../resources/internal'; import { BaseResource, Client, EmailLinkErrorCode, Environment, SignIn, SignUp } from '../resources/internal'; -import { SessionCookieService } from '../services'; import { mockJwt } from '../test/fixtures'; const mockClientFetch = jest.fn(); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index f81d00a8568..1d3a8d627b0 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -85,9 +85,10 @@ import { toURL, windowNavigate, } from '../utils'; -import { getClientUatCookie } from '../utils/cookies/clientUat'; import { memoizeListenerCallback } from '../utils/memoizeStateListenerCallback'; import { CLERK_SATELLITE_URL, CLERK_SYNCED, ERROR_CODES } from './constants'; +import { getClientUatCookie } from './cookies/clientUat'; +import { SessionCookieService } from './cookies/SessionCookieService'; import type { DevBrowser } from './devBrowser'; import { createDevBrowser } from './devBrowser'; import { @@ -110,7 +111,6 @@ import { Environment, Organization, } from './resources/internal'; -import { SessionCookieService } from './services'; import { warnings } from './warnings'; export type ClerkCoreBroadcastChannelEvent = { type: 'signout' }; diff --git a/packages/clerk-js/src/core/services/authentication/SessionCookiePoller.ts b/packages/clerk-js/src/core/cookies/SessionCookiePoller.ts similarity index 94% rename from packages/clerk-js/src/core/services/authentication/SessionCookiePoller.ts rename to packages/clerk-js/src/core/cookies/SessionCookiePoller.ts index 16f6a5df1d5..3d4ed2fc41b 100644 --- a/packages/clerk-js/src/core/services/authentication/SessionCookiePoller.ts +++ b/packages/clerk-js/src/core/cookies/SessionCookiePoller.ts @@ -1,6 +1,6 @@ import { createWorkerTimers } from '@clerk/shared'; -import { SafeLock } from '../../../utils'; +import { SafeLock } from './safeLock'; const REFRESH_SESSION_TOKEN_LOCK_KEY = 'clerk.lock.refreshSessionToken'; const INTERVAL_IN_MS = 5 * 1000; diff --git a/packages/clerk-js/src/core/services/authentication/SessionCookieService.ts b/packages/clerk-js/src/core/cookies/SessionCookieService.ts similarity index 89% rename from packages/clerk-js/src/core/services/authentication/SessionCookieService.ts rename to packages/clerk-js/src/core/cookies/SessionCookieService.ts index da024e4e33c..7c5e94a4033 100644 --- a/packages/clerk-js/src/core/services/authentication/SessionCookieService.ts +++ b/packages/clerk-js/src/core/cookies/SessionCookieService.ts @@ -1,16 +1,17 @@ import { is4xxError, isClerkAPIResponseError, isNetworkError } from '@clerk/shared/error'; import type { Clerk, EnvironmentResource, SessionResource, TokenResource } from '@clerk/types'; -import { inBrowser } from '../../../utils'; -import { setClientUatCookie } from '../../../utils/cookies/clientUat'; -import { removeSessionCookie, setSessionCookie } from '../../../utils/cookies/session'; -import { clerkCoreErrorTokenRefreshFailed } from '../../errors'; -import { eventBus, events } from '../../events'; +import { inBrowser } from '../../utils'; +import { clerkCoreErrorTokenRefreshFailed } from '../errors'; +import { eventBus, events } from '../events'; +import { setClientUatCookie } from './clientUat'; +import { removeSessionCookie, setSessionCookie } from './session'; import { SessionCookiePoller } from './SessionCookiePoller'; export class SessionCookieService { private environment: EnvironmentResource | undefined; private poller: SessionCookiePoller | null = null; + private publishableKey: string; constructor(private clerk: Clerk) { // set cookie on token update @@ -20,6 +21,7 @@ export class SessionCookieService { this.refreshTokenOnVisibilityChange(); this.startPollingForToken(); + this.publishableKey = clerk.publishableKey; } public setEnvironment(environment: EnvironmentResource) { diff --git a/packages/clerk-js/src/utils/cookies/clientUat.ts b/packages/clerk-js/src/core/cookies/clientUat.ts similarity index 100% rename from packages/clerk-js/src/utils/cookies/clientUat.ts rename to packages/clerk-js/src/core/cookies/clientUat.ts diff --git a/packages/clerk-js/src/utils/cookies/devBrowser.ts b/packages/clerk-js/src/core/cookies/devBrowser.ts similarity index 100% rename from packages/clerk-js/src/utils/cookies/devBrowser.ts rename to packages/clerk-js/src/core/cookies/devBrowser.ts diff --git a/packages/clerk-js/src/utils/safeLock.ts b/packages/clerk-js/src/core/cookies/safeLock.ts similarity index 100% rename from packages/clerk-js/src/utils/safeLock.ts rename to packages/clerk-js/src/core/cookies/safeLock.ts diff --git a/packages/clerk-js/src/utils/cookies/session.ts b/packages/clerk-js/src/core/cookies/session.ts similarity index 100% rename from packages/clerk-js/src/utils/cookies/session.ts rename to packages/clerk-js/src/core/cookies/session.ts diff --git a/packages/clerk-js/src/core/devBrowser.ts b/packages/clerk-js/src/core/devBrowser.ts index 41c5d42dc98..b28c8de2159 100644 --- a/packages/clerk-js/src/core/devBrowser.ts +++ b/packages/clerk-js/src/core/devBrowser.ts @@ -3,7 +3,7 @@ import { parseErrors } from '@clerk/shared/error'; import type { ClerkAPIErrorJSON } from '@clerk/types'; import { isDevOrStagingUrl } from '../utils'; -import { getDevBrowserCookie, removeDevBrowserCookie, setDevBrowserCookie } from '../utils/cookies/devBrowser'; +import { getDevBrowserCookie, removeDevBrowserCookie, setDevBrowserCookie } from './cookies/devBrowser'; import { clerkErrorDevInitFailed } from './errors'; import type { FapiClient } from './fapiClient'; diff --git a/packages/clerk-js/src/core/services/index.ts b/packages/clerk-js/src/core/services/index.ts deleted file mode 100644 index ff1ae73c0a0..00000000000 --- a/packages/clerk-js/src/core/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './authentication/SessionCookieService'; diff --git a/packages/clerk-js/src/utils/index.ts b/packages/clerk-js/src/utils/index.ts index 052d5ec98fe..cd04c2fbce3 100644 --- a/packages/clerk-js/src/utils/index.ts +++ b/packages/clerk-js/src/utils/index.ts @@ -17,7 +17,6 @@ export * from './props'; export * from './queryStateParams'; export * from './querystring'; export * from './runtime'; -export * from './safeLock'; export * from './url'; export * from './web3'; export * from './windowNavigate'; From 611467a665e30da91b3319e7d0301b3a9bc7edf0 Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Mon, 8 Apr 2024 19:51:40 +0300 Subject: [PATCH 2/8] feat(clerk-js): Add publishableKey suffix in cookies Cookies affected: `__session`, `__client_uat`, `__clerk_db_jwt` --- .../src/core/__tests__/devBrowser.test.ts | 1 + packages/clerk-js/src/core/clerk.ts | 6 +- .../src/core/cookies/SessionCookieService.ts | 19 ++++--- .../clerk-js/src/core/cookies/clientUat.ts | 56 ++++++++++++------- .../clerk-js/src/core/cookies/devBrowser.ts | 50 ++++++++++++----- packages/clerk-js/src/core/cookies/session.ts | 43 ++++++++++---- packages/clerk-js/src/core/devBrowser.ts | 15 +++-- 7 files changed, 132 insertions(+), 58 deletions(-) diff --git a/packages/clerk-js/src/core/__tests__/devBrowser.test.ts b/packages/clerk-js/src/core/__tests__/devBrowser.test.ts index 2162649b4a5..ef15285f9f0 100644 --- a/packages/clerk-js/src/core/__tests__/devBrowser.test.ts +++ b/packages/clerk-js/src/core/__tests__/devBrowser.test.ts @@ -46,6 +46,7 @@ describe('Thrown errors', () => { const mockFapiClient = mockCreateFapiClient() as FapiClient; const devBrowserHandler = createDevBrowser({ + publishableKey: 'pk_test_d2hpdGUta29hbGEtNDIuY2xlcmsuYWNjb3VudHMuZGV2JA', frontendApi: 'white-koala-42.clerk.accounts.dev', fapiClient: mockFapiClient, }); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 1d3a8d627b0..619ac789b50 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -87,7 +87,7 @@ import { } from '../utils'; import { memoizeListenerCallback } from '../utils/memoizeStateListenerCallback'; import { CLERK_SATELLITE_URL, CLERK_SYNCED, ERROR_CODES } from './constants'; -import { getClientUatCookie } from './cookies/clientUat'; +import { createClientUatCookie } from './cookies/clientUat'; import { SessionCookieService } from './cookies/SessionCookieService'; import type { DevBrowser } from './devBrowser'; import { createDevBrowser } from './devBrowser'; @@ -1336,7 +1336,8 @@ export class Clerk implements ClerkInterface { return false; } - return getClientUatCookie() <= 0; + const clientUatCookieHandler = createClientUatCookie(this.publishableKey); + return clientUatCookieHandler.get() <= 0; }; #shouldRedirectToSatellite = (): boolean => { @@ -1398,6 +1399,7 @@ export class Clerk implements ClerkInterface { * Multi-domain SSO needs this handler to be initiated */ this.#devBrowser = createDevBrowser({ + publishableKey: this.publishableKey, frontendApi: this.frontendApi, fapiClient: this.#fapiClient, }); diff --git a/packages/clerk-js/src/core/cookies/SessionCookieService.ts b/packages/clerk-js/src/core/cookies/SessionCookieService.ts index 7c5e94a4033..68fab2fd93c 100644 --- a/packages/clerk-js/src/core/cookies/SessionCookieService.ts +++ b/packages/clerk-js/src/core/cookies/SessionCookieService.ts @@ -4,14 +4,17 @@ import type { Clerk, EnvironmentResource, SessionResource, TokenResource } from import { inBrowser } from '../../utils'; import { clerkCoreErrorTokenRefreshFailed } from '../errors'; import { eventBus, events } from '../events'; -import { setClientUatCookie } from './clientUat'; -import { removeSessionCookie, setSessionCookie } from './session'; +import type { ClientUatCookieHandler } from './clientUat'; +import { createClientUatCookie } from './clientUat'; +import type { SessionCookieHandler } from './session'; +import { createSessionCookie } from './session'; import { SessionCookiePoller } from './SessionCookiePoller'; export class SessionCookieService { private environment: EnvironmentResource | undefined; private poller: SessionCookiePoller | null = null; - private publishableKey: string; + private clientUatCookieHandler: ClientUatCookieHandler; + private sessionCookieHandler: SessionCookieHandler; constructor(private clerk: Clerk) { // set cookie on token update @@ -21,7 +24,9 @@ export class SessionCookieService { this.refreshTokenOnVisibilityChange(); this.startPollingForToken(); - this.publishableKey = clerk.publishableKey; + + this.clientUatCookieHandler = createClientUatCookie(clerk.publishableKey); + this.sessionCookieHandler = createSessionCookie(clerk.publishableKey); } public setEnvironment(environment: EnvironmentResource) { @@ -73,14 +78,14 @@ export class SessionCookieService { const rawToken = typeof token === 'string' ? token : token?.getRawString(); if (rawToken) { - return setSessionCookie(rawToken); + return this.sessionCookieHandler.set(rawToken); } - return removeSessionCookie(); + return this.sessionCookieHandler.remove(); } private setClientUatCookieForDevelopmentInstances() { if (this.environment && this.environment.isDevelopmentOrStaging() && this.inCustomDevelopmentDomain()) { - setClientUatCookie(this.clerk.client); + this.clientUatCookieHandler.set(this.clerk.client); } } diff --git a/packages/clerk-js/src/core/cookies/clientUat.ts b/packages/clerk-js/src/core/cookies/clientUat.ts index 309259fc4a3..c72e574f085 100644 --- a/packages/clerk-js/src/core/cookies/clientUat.ts +++ b/packages/clerk-js/src/core/cookies/clientUat.ts @@ -6,28 +6,46 @@ import { inCrossOriginIframe } from '../../utils'; const CLIENT_UAT_COOKIE_NAME = '__client_uat'; -export const clientUatCookie = createCookieHandler(CLIENT_UAT_COOKIE_NAME); - -export const getClientUatCookie = (): number => { - return parseInt(clientUatCookie.get() || '0', 10); +export type ClientUatCookieHandler = { + set: (client: ClientResource | undefined) => void; + get: () => number; }; -export const setClientUatCookie = (client: ClientResource | undefined) => { - const expires = addYears(Date.now(), 1); - const sameSite = inCrossOriginIframe() ? 'None' : 'Strict'; - const secure = window.location.protocol === 'https:'; +export const createClientUatCookie = (publishableKey: string, legacy = true): ClientUatCookieHandler => { + const suffix = publishableKey.split('_').pop(); + const legacyClientUatCookie = createCookieHandler(CLIENT_UAT_COOKIE_NAME); + const multipleAppsSessionCookie = createCookieHandler(`${CLIENT_UAT_COOKIE_NAME}_${suffix}`); + + const clientUatCookie = legacy ? legacyClientUatCookie : multipleAppsSessionCookie; + + const get = (): number => { + return parseInt(clientUatCookie.get() || '0', 10); + }; + + const set = (client: ClientResource | undefined) => { + legacyClientUatCookie.remove(); + + const expires = addYears(Date.now(), 1); + const sameSite = inCrossOriginIframe() ? 'None' : 'Strict'; + const secure = window.location.protocol === 'https:'; + + // '0' indicates the user is signed out + let val = '0'; - // '0' indicates the user is signed out - let val = '0'; + if (client && client.updatedAt && client.activeSessions.length > 0) { + // truncate timestamp to seconds, since this is a unix timestamp + val = Math.floor(client.updatedAt.getTime() / 1000).toString(); + } - if (client && client.updatedAt && client.activeSessions.length > 0) { - // truncate timestamp to seconds, since this is a unix timestamp - val = Math.floor(client.updatedAt.getTime() / 1000).toString(); - } + return clientUatCookie.set(val, { + expires, + sameSite, + secure, + }); + }; - return clientUatCookie.set(val, { - expires, - sameSite, - secure, - }); + return { + set, + get, + }; }; diff --git a/packages/clerk-js/src/core/cookies/devBrowser.ts b/packages/clerk-js/src/core/cookies/devBrowser.ts index dfd5adace48..803eed15b5c 100644 --- a/packages/clerk-js/src/core/cookies/devBrowser.ts +++ b/packages/clerk-js/src/core/cookies/devBrowser.ts @@ -4,20 +4,44 @@ import { DEV_BROWSER_JWT_KEY } from '@clerk/shared/devBrowser'; import { inCrossOriginIframe } from '../../utils'; -export const devBrowserCookie = createCookieHandler(DEV_BROWSER_JWT_KEY); +export type DevBrowserCookieHandler = { + set: (jwt: string) => void; + get: () => string | undefined; + remove: () => void; +}; -export const getDevBrowserCookie = () => devBrowserCookie.get(); +export const createDevBrowserCookie = (publishableKey: string, legacy = true): DevBrowserCookieHandler => { + const suffix = publishableKey.split('_').pop(); -export const setDevBrowserCookie = (jwt: string) => { - const expires = addYears(Date.now(), 1); - const sameSite = inCrossOriginIframe() ? 'None' : 'Lax'; - const secure = window.location.protocol === 'https:'; + const legacyDevBrowserCookie = createCookieHandler(DEV_BROWSER_JWT_KEY); + const multipleAppsDevBrowserCookie = createCookieHandler(`${DEV_BROWSER_JWT_KEY}_${suffix}`); - return devBrowserCookie.set(jwt, { - expires, - sameSite, - secure, - }); -}; + const devBrowserCookie = legacy ? legacyDevBrowserCookie : multipleAppsDevBrowserCookie; + + const get = () => devBrowserCookie.get(); + + const set = (jwt: string) => { + legacyDevBrowserCookie.remove(); -export const removeDevBrowserCookie = () => devBrowserCookie.remove(); + const expires = addYears(Date.now(), 1); + const sameSite = inCrossOriginIframe() ? 'None' : 'Lax'; + const secure = window.location.protocol === 'https:'; + + return devBrowserCookie.set(jwt, { + expires, + sameSite, + secure, + }); + }; + + const remove = () => { + legacyDevBrowserCookie.remove(); + devBrowserCookie.remove(); + }; + + return { + get, + set, + remove, + }; +}; diff --git a/packages/clerk-js/src/core/cookies/session.ts b/packages/clerk-js/src/core/cookies/session.ts index edb6afb888d..92630e1f935 100644 --- a/packages/clerk-js/src/core/cookies/session.ts +++ b/packages/clerk-js/src/core/cookies/session.ts @@ -5,23 +5,44 @@ import { inCrossOriginIframe } from '../../utils'; const SESSION_COOKIE_NAME = '__session'; +export type SessionCookieHandler = { + set: (token: string) => void; + remove: () => void; +}; + /** * * This is a short-lived JS cookie used to store the current user JWT. * */ -export const sessionCookie = createCookieHandler(SESSION_COOKIE_NAME); +export const createSessionCookie = (publishableKey: string, legacy = true): SessionCookieHandler => { + const suffix = publishableKey.split('_').pop(); + const legacySessionCookie = createCookieHandler(SESSION_COOKIE_NAME); + const multipleAppsSessionCookie = createCookieHandler(`${SESSION_COOKIE_NAME}_${suffix}`); + + const sessionCookie = legacy ? legacySessionCookie : multipleAppsSessionCookie; + + const remove = () => { + legacySessionCookie.remove(); + sessionCookie.remove(); + }; + + const set = (token: string) => { + legacySessionCookie.remove(); -export const removeSessionCookie = () => sessionCookie.remove(); + const expires = addYears(Date.now(), 1); + const sameSite = inCrossOriginIframe() ? 'None' : 'Lax'; + const secure = window.location.protocol === 'https:'; -export const setSessionCookie = (token: string) => { - const expires = addYears(Date.now(), 1); - const sameSite = inCrossOriginIframe() ? 'None' : 'Lax'; - const secure = window.location.protocol === 'https:'; + return sessionCookie.set(token, { + expires, + sameSite, + secure, + }); + }; - return sessionCookie.set(token, { - expires, - sameSite, - secure, - }); + return { + set, + remove, + }; }; diff --git a/packages/clerk-js/src/core/devBrowser.ts b/packages/clerk-js/src/core/devBrowser.ts index b28c8de2159..a19db034057 100644 --- a/packages/clerk-js/src/core/devBrowser.ts +++ b/packages/clerk-js/src/core/devBrowser.ts @@ -3,7 +3,7 @@ import { parseErrors } from '@clerk/shared/error'; import type { ClerkAPIErrorJSON } from '@clerk/types'; import { isDevOrStagingUrl } from '../utils'; -import { getDevBrowserCookie, removeDevBrowserCookie, setDevBrowserCookie } from './cookies/devBrowser'; +import { createDevBrowserCookie } from './cookies/devBrowser'; import { clerkErrorDevInitFailed } from './errors'; import type { FapiClient } from './fapiClient'; @@ -20,21 +20,24 @@ export interface DevBrowser { } export type CreateDevBrowserOptions = { + publishableKey: string; frontendApi: string; fapiClient: FapiClient; }; -export function createDevBrowser({ frontendApi, fapiClient }: CreateDevBrowserOptions): DevBrowser { +export function createDevBrowser({ publishableKey, frontendApi, fapiClient }: CreateDevBrowserOptions): DevBrowser { + const devBrowserCookie = createDevBrowserCookie(publishableKey); + function getDevBrowserJWT() { - return getDevBrowserCookie(); + return devBrowserCookie.get(); } function setDevBrowserJWT(jwt: string) { - setDevBrowserCookie(jwt); + devBrowserCookie.set(jwt); } function removeDevBrowserJWT() { - removeDevBrowserCookie(); + devBrowserCookie.remove(); } function clear() { @@ -69,7 +72,7 @@ export function createDevBrowser({ frontendApi, fapiClient }: CreateDevBrowserOp } // 2. If no JWT is found in the first step, check if a JWT is already available in the __clerk_db_jwt JS cookie - if (getDevBrowserCookie()) { + if (devBrowserCookie.get()) { return; } From cf0ccb482ae2699f7bb0f6c43a64b8481c24c59d Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Mon, 8 Apr 2024 21:56:03 +0300 Subject: [PATCH 3/8] feat(backend): Support publishableKey suffix in cookies --- .../backend/src/tokens/authenticateContext.ts | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/packages/backend/src/tokens/authenticateContext.ts b/packages/backend/src/tokens/authenticateContext.ts index 7ecf5f48492..c44712c824e 100644 --- a/packages/backend/src/tokens/authenticateContext.ts +++ b/packages/backend/src/tokens/authenticateContext.ts @@ -39,6 +39,11 @@ class AuthenticateContext { } public constructor(private clerkRequest: ClerkRequest, options: AuthenticateRequestOptions) { + // Even though the options are assigned to this later in this function + // we set the publishableKey here because it is being used in cookies/headers/handshake-values + // as part of getMultipleAppsCookie + this.publishableKey = options.publishableKey; + this.initHeaderValues(); this.initCookieValues(); this.initHandshakeValues(); @@ -48,35 +53,49 @@ class AuthenticateContext { private initHandshakeValues() { this.devBrowserToken = - this.clerkRequest.clerkUrl.searchParams.get(constants.Cookies.DevBrowser) || - this.clerkRequest.cookies.get(constants.Cookies.DevBrowser); + this.getQueryParam(constants.Cookies.DevBrowser) || this.getMultipleAppsCookie(constants.Cookies.DevBrowser); this.handshakeToken = - this.clerkRequest.clerkUrl.searchParams.get(constants.Cookies.Handshake) || - this.clerkRequest.cookies.get(constants.Cookies.Handshake); + this.getQueryParam(constants.Cookies.Handshake) || this.getCookie(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); + this.sessionTokenInHeader = this.stripAuthorizationHeader(this.getHeader(constants.Headers.Authorization)); + this.origin = this.getHeader(constants.Headers.Origin); + this.host = this.getHeader(constants.Headers.Host); + this.forwardedHost = this.getHeader(constants.Headers.ForwardedHost); + this.forwardedProto = + this.getHeader(constants.Headers.CloudFrontForwardedProto) || this.getHeader(constants.Headers.ForwardedProto); + this.referrer = this.getHeader(constants.Headers.Referrer); + this.userAgent = this.getHeader(constants.Headers.UserAgent); + this.secFetchDest = this.getHeader(constants.Headers.SecFetchDest); + this.accept = this.getHeader(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; + this.sessionTokenInCookie = this.getMultipleAppsCookie(constants.Cookies.Session); + this.clientUat = Number.parseInt(this.getMultipleAppsCookie(constants.Cookies.ClientUat) || '') || 0; } private stripAuthorizationHeader(authValue: string | undefined | null): string | undefined { return authValue?.replace('Bearer ', ''); } + + private getQueryParam(name: string) { + return this.clerkRequest.clerkUrl.searchParams.get(name); + } + + private getHeader(name: string) { + return this.clerkRequest.headers.get(name) || undefined; + } + + private getCookie(name: string) { + return this.clerkRequest.cookies.get(name) || undefined; + } + + private getMultipleAppsCookie(cookieName: string) { + const suffix = this.publishableKey?.split('_').pop(); + return this.getCookie(`${cookieName}_${suffix}`) || this.getCookie(cookieName) || undefined; + } } export type { AuthenticateContext }; From 20e0ba7e8b3e7abfc7b3f15323137a3aefae0ede Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Tue, 9 Apr 2024 16:36:10 +0300 Subject: [PATCH 4/8] chore(clerk-js): Rename cookies module to auth + extract session code from Clerk --- .../clerk-js/src/core/__tests__/clerk.test.ts | 4 +- .../{cookies => auth}/SessionCookiePoller.ts | 2 +- .../{cookies => auth}/SessionCookieService.ts | 68 +++++++--- .../{ => auth}/__tests__/devBrowser.test.ts | 2 +- .../src/core/{ => auth}/cookies/clientUat.ts | 2 +- .../src/core/{ => auth}/cookies/devBrowser.ts | 2 +- .../src/core/{ => auth}/cookies/safeLock.ts | 0 .../src/core/{ => auth}/cookies/session.ts | 2 +- .../src/core/{ => auth}/devBrowser.ts | 6 +- packages/clerk-js/src/core/clerk.ts | 116 +++++++++--------- 10 files changed, 115 insertions(+), 89 deletions(-) rename packages/clerk-js/src/core/{cookies => auth}/SessionCookiePoller.ts (94%) rename packages/clerk-js/src/core/{cookies => auth}/SessionCookieService.ts (55%) rename packages/clerk-js/src/core/{ => auth}/__tests__/devBrowser.test.ts (97%) rename packages/clerk-js/src/core/{ => auth}/cookies/clientUat.ts (96%) rename packages/clerk-js/src/core/{ => auth}/cookies/devBrowser.ts (95%) rename packages/clerk-js/src/core/{ => auth}/cookies/safeLock.ts (100%) rename packages/clerk-js/src/core/{ => auth}/cookies/session.ts (95%) rename packages/clerk-js/src/core/{ => auth}/devBrowser.ts (94%) diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index 6788c14d1be..836c2af1533 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -2,9 +2,9 @@ import type { ActiveSessionResource, SignInJSON, SignUpJSON, TokenResource } fro import { waitFor } from '@testing-library/dom'; import { mockNativeRuntime } from '../../testUtils'; +import type { DevBrowser } from '../auth/devBrowser'; +import { SessionCookieService } from '../auth/SessionCookieService'; import { Clerk } from '../clerk'; -import { SessionCookieService } from '../cookies/SessionCookieService'; -import type { DevBrowser } from '../devBrowser'; import { eventBus, events } from '../events'; import type { DisplayConfig, Organization } from '../resources/internal'; import { BaseResource, Client, EmailLinkErrorCode, Environment, SignIn, SignUp } from '../resources/internal'; diff --git a/packages/clerk-js/src/core/cookies/SessionCookiePoller.ts b/packages/clerk-js/src/core/auth/SessionCookiePoller.ts similarity index 94% rename from packages/clerk-js/src/core/cookies/SessionCookiePoller.ts rename to packages/clerk-js/src/core/auth/SessionCookiePoller.ts index 3d4ed2fc41b..74c33374700 100644 --- a/packages/clerk-js/src/core/cookies/SessionCookiePoller.ts +++ b/packages/clerk-js/src/core/auth/SessionCookiePoller.ts @@ -1,6 +1,6 @@ import { createWorkerTimers } from '@clerk/shared'; -import { SafeLock } from './safeLock'; +import { SafeLock } from './cookies/safeLock'; const REFRESH_SESSION_TOKEN_LOCK_KEY = 'clerk.lock.refreshSessionToken'; const INTERVAL_IN_MS = 5 * 1000; diff --git a/packages/clerk-js/src/core/cookies/SessionCookieService.ts b/packages/clerk-js/src/core/auth/SessionCookieService.ts similarity index 55% rename from packages/clerk-js/src/core/cookies/SessionCookieService.ts rename to packages/clerk-js/src/core/auth/SessionCookieService.ts index 68fab2fd93c..d6c6967694f 100644 --- a/packages/clerk-js/src/core/cookies/SessionCookieService.ts +++ b/packages/clerk-js/src/core/auth/SessionCookieService.ts @@ -1,32 +1,43 @@ +import { setDevBrowserJWTInURL } from '@clerk/shared/devBrowser'; import { is4xxError, isClerkAPIResponseError, isNetworkError } from '@clerk/shared/error'; -import type { Clerk, EnvironmentResource, SessionResource, TokenResource } from '@clerk/types'; +import type { Clerk, EnvironmentResource, TokenResource } from '@clerk/types'; import { inBrowser } from '../../utils'; -import { clerkCoreErrorTokenRefreshFailed } from '../errors'; +import { clerkCoreErrorTokenRefreshFailed, clerkMissingDevBrowserJwt } from '../errors'; import { eventBus, events } from '../events'; -import type { ClientUatCookieHandler } from './clientUat'; -import { createClientUatCookie } from './clientUat'; -import type { SessionCookieHandler } from './session'; -import { createSessionCookie } from './session'; +import type { FapiClient } from '../fapiClient'; +import type { ClientUatCookieHandler } from './cookies/clientUat'; +import { createClientUatCookie } from './cookies/clientUat'; +import type { SessionCookieHandler } from './cookies/session'; +import { createSessionCookie } from './cookies/session'; +import type { DevBrowser } from './devBrowser'; +import { createDevBrowser } from './devBrowser'; import { SessionCookiePoller } from './SessionCookiePoller'; export class SessionCookieService { private environment: EnvironmentResource | undefined; private poller: SessionCookiePoller | null = null; - private clientUatCookieHandler: ClientUatCookieHandler; - private sessionCookieHandler: SessionCookieHandler; + private clientUat: ClientUatCookieHandler; + private sessionCookie: SessionCookieHandler; + private devBrowser: DevBrowser; - constructor(private clerk: Clerk) { + constructor(private clerk: Clerk, fapiClient: FapiClient) { // set cookie on token update eventBus.on(events.TokenUpdate, ({ token }) => { this.updateSessionCookie(token?.getRawString()); + this.setClientUatCookieForDevelopmentInstances(); }); this.refreshTokenOnVisibilityChange(); this.startPollingForToken(); - this.clientUatCookieHandler = createClientUatCookie(clerk.publishableKey); - this.sessionCookieHandler = createSessionCookie(clerk.publishableKey); + this.clientUat = createClientUatCookie(clerk.publishableKey); + this.sessionCookie = createSessionCookie(clerk.publishableKey); + this.devBrowser = createDevBrowser({ + publishableKey: clerk.publishableKey, + frontendApi: clerk.frontendApi, + fapiClient, + }); } public setEnvironment(environment: EnvironmentResource) { @@ -34,9 +45,30 @@ export class SessionCookieService { this.setClientUatCookieForDevelopmentInstances(); } - public async setAuthCookiesFromSession(session: SessionResource | undefined | null): Promise { - this.updateSessionCookie(await session?.getToken()); - this.setClientUatCookieForDevelopmentInstances(); + public isSignedOut() { + return this.clientUat.get() <= 0; + } + + public async setupDevelopment() { + await this.devBrowser.setup(); + } + + public setupProduction() { + this.devBrowser.clear(); + } + + public async handleUnauthenticatedDevBrowser() { + this.devBrowser.clear(); + await this.devBrowser.setup(); + } + + public urlWithAuth(url: URL): URL { + const devBrowserJwt = this.devBrowser.getDevBrowserJWT(); + if (!devBrowserJwt) { + return clerkMissingDevBrowserJwt(); + } + + return setDevBrowserJWTInURL(url, devBrowserJwt); } private startPollingForToken() { @@ -78,14 +110,14 @@ export class SessionCookieService { const rawToken = typeof token === 'string' ? token : token?.getRawString(); if (rawToken) { - return this.sessionCookieHandler.set(rawToken); + return this.sessionCookie.set(rawToken); } - return this.sessionCookieHandler.remove(); + return this.sessionCookie.remove(); } private setClientUatCookieForDevelopmentInstances() { - if (this.environment && this.environment.isDevelopmentOrStaging() && this.inCustomDevelopmentDomain()) { - this.clientUatCookieHandler.set(this.clerk.client); + if (this.environment?.isDevelopmentOrStaging() && this.inCustomDevelopmentDomain()) { + this.clientUat.set(this.clerk.client); } } diff --git a/packages/clerk-js/src/core/__tests__/devBrowser.test.ts b/packages/clerk-js/src/core/auth/__tests__/devBrowser.test.ts similarity index 97% rename from packages/clerk-js/src/core/__tests__/devBrowser.test.ts rename to packages/clerk-js/src/core/auth/__tests__/devBrowser.test.ts index ef15285f9f0..179b3bd4d0d 100644 --- a/packages/clerk-js/src/core/__tests__/devBrowser.test.ts +++ b/packages/clerk-js/src/core/auth/__tests__/devBrowser.test.ts @@ -1,5 +1,5 @@ +import type { FapiClient } from '../../fapiClient'; import { createDevBrowser } from '../devBrowser'; -import type { FapiClient } from '../fapiClient'; type RecursivePartial = { [P in keyof T]?: RecursivePartial; diff --git a/packages/clerk-js/src/core/cookies/clientUat.ts b/packages/clerk-js/src/core/auth/cookies/clientUat.ts similarity index 96% rename from packages/clerk-js/src/core/cookies/clientUat.ts rename to packages/clerk-js/src/core/auth/cookies/clientUat.ts index c72e574f085..f353292fe3b 100644 --- a/packages/clerk-js/src/core/cookies/clientUat.ts +++ b/packages/clerk-js/src/core/auth/cookies/clientUat.ts @@ -2,7 +2,7 @@ import { createCookieHandler } from '@clerk/shared/cookie'; import { addYears } from '@clerk/shared/date'; import type { ClientResource } from '@clerk/types'; -import { inCrossOriginIframe } from '../../utils'; +import { inCrossOriginIframe } from '../../../utils'; const CLIENT_UAT_COOKIE_NAME = '__client_uat'; diff --git a/packages/clerk-js/src/core/cookies/devBrowser.ts b/packages/clerk-js/src/core/auth/cookies/devBrowser.ts similarity index 95% rename from packages/clerk-js/src/core/cookies/devBrowser.ts rename to packages/clerk-js/src/core/auth/cookies/devBrowser.ts index 803eed15b5c..2f9b7315253 100644 --- a/packages/clerk-js/src/core/cookies/devBrowser.ts +++ b/packages/clerk-js/src/core/auth/cookies/devBrowser.ts @@ -2,7 +2,7 @@ import { createCookieHandler } from '@clerk/shared/cookie'; import { addYears } from '@clerk/shared/date'; import { DEV_BROWSER_JWT_KEY } from '@clerk/shared/devBrowser'; -import { inCrossOriginIframe } from '../../utils'; +import { inCrossOriginIframe } from '../../../utils'; export type DevBrowserCookieHandler = { set: (jwt: string) => void; diff --git a/packages/clerk-js/src/core/cookies/safeLock.ts b/packages/clerk-js/src/core/auth/cookies/safeLock.ts similarity index 100% rename from packages/clerk-js/src/core/cookies/safeLock.ts rename to packages/clerk-js/src/core/auth/cookies/safeLock.ts diff --git a/packages/clerk-js/src/core/cookies/session.ts b/packages/clerk-js/src/core/auth/cookies/session.ts similarity index 95% rename from packages/clerk-js/src/core/cookies/session.ts rename to packages/clerk-js/src/core/auth/cookies/session.ts index 92630e1f935..012e320072c 100644 --- a/packages/clerk-js/src/core/cookies/session.ts +++ b/packages/clerk-js/src/core/auth/cookies/session.ts @@ -1,7 +1,7 @@ import { createCookieHandler } from '@clerk/shared/cookie'; import { addYears } from '@clerk/shared/date'; -import { inCrossOriginIframe } from '../../utils'; +import { inCrossOriginIframe } from '../../../utils'; const SESSION_COOKIE_NAME = '__session'; diff --git a/packages/clerk-js/src/core/devBrowser.ts b/packages/clerk-js/src/core/auth/devBrowser.ts similarity index 94% rename from packages/clerk-js/src/core/devBrowser.ts rename to packages/clerk-js/src/core/auth/devBrowser.ts index a19db034057..bf5ee58c183 100644 --- a/packages/clerk-js/src/core/devBrowser.ts +++ b/packages/clerk-js/src/core/auth/devBrowser.ts @@ -2,10 +2,10 @@ import { DEV_BROWSER_JWT_HEADER, extractDevBrowserJWTFromURL, setDevBrowserJWTIn import { parseErrors } from '@clerk/shared/error'; import type { ClerkAPIErrorJSON } from '@clerk/types'; -import { isDevOrStagingUrl } from '../utils'; +import { isDevOrStagingUrl } from '../../utils'; +import { clerkErrorDevInitFailed } from '../errors'; +import type { FapiClient } from '../fapiClient'; import { createDevBrowserCookie } from './cookies/devBrowser'; -import { clerkErrorDevInitFailed } from './errors'; -import type { FapiClient } from './fapiClient'; export interface DevBrowser { clear(): void; diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 619ac789b50..628473651ec 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -11,7 +11,6 @@ import { noop, parsePublishableKey, proxyUrlToAbsoluteURL, - setDevBrowserJWTInURL, stripScheme, } from '@clerk/shared'; import { eventComponentMounted, TelemetryCollector } from '@clerk/shared/telemetry'; @@ -86,21 +85,18 @@ import { windowNavigate, } from '../utils'; import { memoizeListenerCallback } from '../utils/memoizeStateListenerCallback'; +import { SessionCookieService } from './auth/SessionCookieService'; import { CLERK_SATELLITE_URL, CLERK_SYNCED, ERROR_CODES } from './constants'; -import { createClientUatCookie } from './cookies/clientUat'; -import { SessionCookieService } from './cookies/SessionCookieService'; -import type { DevBrowser } from './devBrowser'; -import { createDevBrowser } from './devBrowser'; import { clerkErrorInitFailed, clerkInvalidSignInUrlFormat, clerkInvalidSignInUrlOrigin, - clerkMissingDevBrowserJwt, clerkMissingProxyUrlAndDomain, clerkMissingSignInUrlAsSatellite, clerkOAuthCallbackDidNotCompleteSignInSignUp, clerkRedirectUrlIsMissingScheme, } from './errors'; +import { eventBus, events } from './events'; import type { FapiClient, FapiRequestCallback } from './fapiClient'; import { createFapiClient } from './fapiClient'; import { @@ -162,8 +158,8 @@ export class Clerk implements ClerkInterface { #authService: SessionCookieService | null = null; #broadcastChannel: LocalStorageBroadcastChannel | null = null; #componentControls?: ReturnType | null; - #devBrowser: DevBrowser | null = null; - #environment?: EnvironmentResource | null; + // converted to protected environment to support `updateEnvironment` type assertion + protected environment?: EnvironmentResource | null; //@ts-expect-error with being undefined even though it's not possible - related to issue with ts and error thrower #fapiClient: FapiClient; #instanceType?: InstanceType; @@ -260,6 +256,7 @@ export class Clerk implements ClerkInterface { this.#instanceType = publishableKey.instanceType; this.#fapiClient = createFapiClient(this); + // This line is used for the piggy-backing mechanism BaseResource.clerk = this; } @@ -331,7 +328,7 @@ export class Clerk implements ClerkInterface { public openSignIn = (props?: SignInProps): void => { this.assertComponentsReady(this.#componentControls); - if (sessionExistsAndSingleSessionModeEnabled(this, this.#environment)) { + if (sessionExistsAndSingleSessionModeEnabled(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotOpenSignInOrSignUp, { code: 'cannot_render_single_session_enabled', @@ -351,7 +348,7 @@ export class Clerk implements ClerkInterface { public openSignUp = (props?: SignUpProps): void => { this.assertComponentsReady(this.#componentControls); - if (sessionExistsAndSingleSessionModeEnabled(this, this.#environment)) { + if (sessionExistsAndSingleSessionModeEnabled(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotOpenSignInOrSignUp, { code: 'cannot_render_single_session_enabled', @@ -391,7 +388,7 @@ export class Clerk implements ClerkInterface { public openOrganizationProfile = (props?: OrganizationProfileProps): void => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.#environment)) { + if (disabledOrganizationsFeature(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationProfile'), { code: 'cannot_render_organizations_disabled', @@ -419,7 +416,7 @@ export class Clerk implements ClerkInterface { public openCreateOrganization = (props?: CreateOrganizationProps): void => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.#environment)) { + if (disabledOrganizationsFeature(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('CreateOrganization'), { code: 'cannot_render_organizations_disabled', @@ -514,7 +511,7 @@ export class Clerk implements ClerkInterface { public mountOrganizationProfile = (node: HTMLDivElement, props?: OrganizationProfileProps) => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.#environment)) { + if (disabledOrganizationsFeature(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationProfile'), { code: 'cannot_render_organizations_disabled', @@ -553,7 +550,7 @@ export class Clerk implements ClerkInterface { public mountCreateOrganization = (node: HTMLDivElement, props?: CreateOrganizationProps) => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.#environment)) { + if (disabledOrganizationsFeature(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('CreateOrganization'), { code: 'cannot_render_organizations_disabled', @@ -584,7 +581,7 @@ export class Clerk implements ClerkInterface { public mountOrganizationSwitcher = (node: HTMLDivElement, props?: OrganizationSwitcherProps) => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.#environment)) { + if (disabledOrganizationsFeature(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationSwitcher'), { code: 'cannot_render_organizations_disabled', @@ -611,7 +608,7 @@ export class Clerk implements ClerkInterface { public mountOrganizationList = (node: HTMLDivElement, props?: OrganizationListProps) => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.#environment)) { + if (disabledOrganizationsFeature(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationList'), { code: 'cannot_render_organizations_disabled', @@ -715,7 +712,13 @@ export class Clerk implements ClerkInterface { newSession = this.#getSessionFromClient(newSession?.id); } - await this.#authService?.setAuthCookiesFromSession(newSession); + // Sync __session and __client_uat to cookies using events.TokenUpdate dispatched event + // only for newSession is null since the getToken will not be executed. Since getToken + // triggers internally a events.TokenUpdate there is no need to trigger it when the newSession exists. + const token = await newSession?.getToken(); + if (!token) { + eventBus.dispatch(events.TokenUpdate, { token: null }); + } //2. If there's a beforeEmit, typically we're navigating. Emit the session as // undefined, then wait for beforeEmit to complete before emitting the new session. @@ -793,12 +796,11 @@ export class Clerk implements ClerkInterface { return toURL.href; } - const devBrowserJwt = this.#devBrowser?.getDevBrowserJWT(); - if (!devBrowserJwt) { - return clerkMissingDevBrowserJwt(); + if (!this.#authService) { + return toURL.href; } - return setDevBrowserJWTInURL(toURL, devBrowserJwt).href; + return this.#authService.urlWithAuth(toURL).href; } public buildSignInUrl(options?: SignInRedirectOptions): string { @@ -810,17 +812,17 @@ export class Clerk implements ClerkInterface { } public buildUserProfileUrl(): string { - if (!this.#environment || !this.#environment.displayConfig) { + if (!this.environment || !this.environment.displayConfig) { return ''; } - return this.buildUrlWithAuth(this.#environment.displayConfig.userProfileUrl); + return this.buildUrlWithAuth(this.environment.displayConfig.userProfileUrl); } public buildHomeUrl(): string { - if (!this.#environment || !this.#environment.displayConfig) { + if (!this.environment || !this.environment.displayConfig) { return ''; } - return this.buildUrlWithAuth(this.#environment.displayConfig.homeUrl); + return this.buildUrlWithAuth(this.environment.displayConfig.homeUrl); } public buildAfterSignInUrl(): string { @@ -848,17 +850,17 @@ export class Clerk implements ClerkInterface { } public buildCreateOrganizationUrl(): string { - if (!this.#environment || !this.#environment.displayConfig) { + if (!this.environment || !this.environment.displayConfig) { return ''; } - return this.buildUrlWithAuth(this.#environment.displayConfig.createOrganizationUrl); + return this.buildUrlWithAuth(this.environment.displayConfig.createOrganizationUrl); } public buildOrganizationProfileUrl(): string { - if (!this.#environment || !this.#environment.displayConfig) { + if (!this.environment || !this.environment.displayConfig) { return ''; } - return this.buildUrlWithAuth(this.#environment.displayConfig.organizationProfileUrl); + return this.buildUrlWithAuth(this.environment.displayConfig.organizationProfileUrl); } #redirectToSatellite = async (): Promise => { @@ -991,11 +993,11 @@ export class Clerk implements ClerkInterface { params: HandleOAuthCallbackParams = {}, customNavigate?: (to: string) => Promise, ): Promise => { - if (!this.loaded || !this.#environment || !this.client) { + if (!this.loaded || !this.environment || !this.client) { return; } const { signIn, signUp } = this.client; - const { displayConfig } = this.#environment; + const { displayConfig } = this.environment; const { firstFactorVerification } = signIn; const { externalAccount } = signUp.verifications; const su = { @@ -1182,7 +1184,7 @@ export class Clerk implements ClerkInterface { customNavigate, unsafeMetadata, }: AuthenticateWithMetamaskParams = {}): Promise => { - if (!this.client || !this.#environment) { + if (!this.client || !this.environment) { return; } @@ -1228,8 +1230,8 @@ export class Clerk implements ClerkInterface { public getOrganization = async (organizationId: string): Promise => Organization.get(organizationId); - public updateEnvironment(environment: EnvironmentResource) { - this.#environment = environment; + public updateEnvironment(environment: EnvironmentResource): asserts this is { environment: EnvironmentResource } { + this.environment = environment; this.#authService?.setEnvironment(environment); } @@ -1269,16 +1271,16 @@ export class Clerk implements ClerkInterface { }; get __unstable__environment(): EnvironmentResource | null | undefined { - return this.#environment; + return this.environment; } // TODO: Fix this properly // eslint-disable-next-line @typescript-eslint/require-await __unstable__setEnvironment = async (env: EnvironmentJSON) => { - this.#environment = new Environment(env); + this.environment = new Environment(env); if (Clerk.mountComponentRenderer) { - this.#componentControls = Clerk.mountComponentRenderer(this, this.#environment, this.#options); + this.#componentControls = Clerk.mountComponentRenderer(this, this.environment, this.#options); } }; @@ -1336,8 +1338,7 @@ export class Clerk implements ClerkInterface { return false; } - const clientUatCookieHandler = createClientUatCookie(this.publishableKey); - return clientUatCookieHandler.get() <= 0; + return !!this.#authService?.isSignedOut(); }; #shouldRedirectToSatellite = (): boolean => { @@ -1393,17 +1394,13 @@ export class Clerk implements ClerkInterface { }; #loadInStandardBrowser = async (): Promise => { + this.#authService = new SessionCookieService(this, this.#fapiClient); + /** * 1. Create the devBrowser. * At this point the devBrowser is not yet setup, but its API is ready for use * Multi-domain SSO needs this handler to be initiated */ - this.#devBrowser = createDevBrowser({ - publishableKey: this.publishableKey, - frontendApi: this.frontendApi, - fapiClient: this.#fapiClient, - }); - /** * 2. Multi-domain SSO handling * If needed the app will attempt to sync with another app hosted in a different domain in order to acquire a session @@ -1423,9 +1420,9 @@ export class Clerk implements ClerkInterface { * For multi-domain we want to avoid retrieving a fresh JWT from FAPI, and we need to get the token as a result of multi-domain session syncing. */ if (this.#instanceType === 'production') { - this.#devBrowser.clear(); + this.#authService?.setupProduction(); } else { - await this.#devBrowser.setup(); + await this.#authService?.setupDevelopment(); } /** @@ -1443,21 +1440,19 @@ export class Clerk implements ClerkInterface { * - Fetch & update client * - Mount components */ - this.#authService = new SessionCookieService(this); this.#pageLifecycle = createPageLifecycle(); - const isInAccountsHostedPages = isDevAccountPortalOrigin(window?.location.hostname); - this.#broadcastChannel = new LocalStorageBroadcastChannel('clerk'); this.#setupListeners(); + const isInAccountsHostedPages = isDevAccountPortalOrigin(window?.location.hostname); + const shouldTouchEnv = this.#instanceType === 'development' && !isInAccountsHostedPages; + let retries = 0; while (retries < 2) { retries++; try { - const shouldTouchEnv = this.#instanceType === 'development' && !isInAccountsHostedPages; - const [environment, client] = await Promise.all([ Environment.getInstance().fetch({ touch: shouldTouchEnv }), Client.getInstance().fetch(), @@ -1474,14 +1469,13 @@ export class Clerk implements ClerkInterface { } if (Clerk.mountComponentRenderer) { - this.#componentControls = Clerk.mountComponentRenderer(this, this.#environment as Environment, this.#options); + this.#componentControls = Clerk.mountComponentRenderer(this, this.environment, this.#options); } break; } catch (err) { if (isError(err, 'dev_browser_unauthenticated')) { - this.#devBrowser.clear(); - await this.#devBrowser.setup(); + await this.#authService.handleUnauthenticatedDevBrowser(); } else if (!isValidBrowserOnline()) { console.warn(err); return false; @@ -1507,13 +1501,13 @@ export class Clerk implements ClerkInterface { Client.getInstance().fetch(), ]); - this.#environment = environment; this.updateClient(client); + this.updateEnvironment(environment); // TODO: Add an auth service also for non standard browsers that will poll for the __session JWT but won't use cookies if (Clerk.mountComponentRenderer) { - this.#componentControls = Clerk.mountComponentRenderer(this, this.#environment, this.#options); + this.#componentControls = Clerk.mountComponentRenderer(this, this.environment, this.#options); } return true; @@ -1622,13 +1616,13 @@ export class Clerk implements ClerkInterface { }; #buildUrl = (key: 'signInUrl' | 'signUpUrl', options?: SignInRedirectOptions | SignUpRedirectOptions): string => { - if (!this.loaded || !this.#environment || !this.#environment.displayConfig) { + if (!this.loaded || !this.environment || !this.environment.displayConfig) { return ''; } const signInOrUpUrl = pickRedirectionProp( key, - { options: this.#options, displayConfig: this.#environment.displayConfig }, + { options: this.#options, displayConfig: this.environment.displayConfig }, false, ); @@ -1667,9 +1661,9 @@ export class Clerk implements ClerkInterface { } const userSignedIn = this.session; - const signInUrl = this.#options.signInUrl || this.#environment?.displayConfig.signInUrl; + const signInUrl = this.#options.signInUrl || this.environment?.displayConfig.signInUrl; const referrerIsSignInUrl = signInUrl && window.location.href.startsWith(signInUrl); - const signUpUrl = this.#options.signUpUrl || this.#environment?.displayConfig.signUpUrl; + const signUpUrl = this.#options.signUpUrl || this.environment?.displayConfig.signUpUrl; const referrerIsSignUpUrl = signUpUrl && window.location.href.startsWith(signUpUrl); // don't redirect if user is not signed in and referrer is sign in/up url From caf0bbcf14f64b360ab0508d66259349848629fc Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Tue, 9 Apr 2024 17:57:36 +0300 Subject: [PATCH 5/8] feat(clerk-js): Introduce mechanism to migrate to cookies with suffix --- .../clerk-js/src/core/__tests__/clerk.test.ts | 1 + .../src/core/auth/SessionCookiePoller.ts | 2 +- .../src/core/auth/SessionCookieService.ts | 27 +++++++++++++------ .../src/core/auth/cookies/clientUat.ts | 22 ++++++++++----- .../src/core/auth/cookies/devBrowser.ts | 24 ++++++++++++----- .../clerk-js/src/core/auth/cookies/session.ts | 23 +++++++++++----- packages/clerk-js/src/core/auth/devBrowser.ts | 14 ++++++++-- .../src/core/auth/{cookies => }/safeLock.ts | 0 8 files changed, 82 insertions(+), 31 deletions(-) rename packages/clerk-js/src/core/auth/{cookies => }/safeLock.ts (100%) diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index 836c2af1533..f4204827655 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -19,6 +19,7 @@ jest.mock('../resources/Environment'); // Because Jest, don't ask me why... jest.mock('../devBrowser', () => ({ createDevBrowser: (): DevBrowser => ({ + migrate: jest.fn(), clear: jest.fn(), setup: jest.fn(), getDevBrowserJWT: jest.fn(() => 'deadbeef'), diff --git a/packages/clerk-js/src/core/auth/SessionCookiePoller.ts b/packages/clerk-js/src/core/auth/SessionCookiePoller.ts index 74c33374700..3d4ed2fc41b 100644 --- a/packages/clerk-js/src/core/auth/SessionCookiePoller.ts +++ b/packages/clerk-js/src/core/auth/SessionCookiePoller.ts @@ -1,6 +1,6 @@ import { createWorkerTimers } from '@clerk/shared'; -import { SafeLock } from './cookies/safeLock'; +import { SafeLock } from './safeLock'; const REFRESH_SESSION_TOKEN_LOCK_KEY = 'clerk.lock.refreshSessionToken'; const INTERVAL_IN_MS = 5 * 1000; diff --git a/packages/clerk-js/src/core/auth/SessionCookieService.ts b/packages/clerk-js/src/core/auth/SessionCookieService.ts index d6c6967694f..5c9fb7c545c 100644 --- a/packages/clerk-js/src/core/auth/SessionCookieService.ts +++ b/packages/clerk-js/src/core/auth/SessionCookieService.ts @@ -21,7 +21,7 @@ export class SessionCookieService { private sessionCookie: SessionCookieHandler; private devBrowser: DevBrowser; - constructor(private clerk: Clerk, fapiClient: FapiClient) { + constructor(private clerk: Clerk, fapiClient: FapiClient, multipleAppsSameDomainEnabled = false) { // set cookie on token update eventBus.on(events.TokenUpdate, ({ token }) => { this.updateSessionCookie(token?.getRawString()); @@ -31,13 +31,18 @@ export class SessionCookieService { this.refreshTokenOnVisibilityChange(); this.startPollingForToken(); - this.clientUat = createClientUatCookie(clerk.publishableKey); - this.sessionCookie = createSessionCookie(clerk.publishableKey); - this.devBrowser = createDevBrowser({ - publishableKey: clerk.publishableKey, - frontendApi: clerk.frontendApi, - fapiClient, - }); + this.clientUat = createClientUatCookie(clerk.publishableKey, multipleAppsSameDomainEnabled); + this.sessionCookie = createSessionCookie(clerk.publishableKey, multipleAppsSameDomainEnabled); + this.devBrowser = createDevBrowser( + { + publishableKey: clerk.publishableKey, + frontendApi: clerk.frontendApi, + fapiClient, + }, + multipleAppsSameDomainEnabled, + ); + + this.migrateCookieValues(); } public setEnvironment(environment: EnvironmentResource) { @@ -71,6 +76,12 @@ export class SessionCookieService { return setDevBrowserJWTInURL(url, devBrowserJwt); } + private migrateCookieValues() { + this.clientUat.migrate(); + this.sessionCookie.migrate(); + this.devBrowser.migrate(); + } + private startPollingForToken() { if (!this.poller) { this.poller = new SessionCookiePoller(); diff --git a/packages/clerk-js/src/core/auth/cookies/clientUat.ts b/packages/clerk-js/src/core/auth/cookies/clientUat.ts index f353292fe3b..5b96202b495 100644 --- a/packages/clerk-js/src/core/auth/cookies/clientUat.ts +++ b/packages/clerk-js/src/core/auth/cookies/clientUat.ts @@ -9,22 +9,21 @@ const CLIENT_UAT_COOKIE_NAME = '__client_uat'; export type ClientUatCookieHandler = { set: (client: ClientResource | undefined) => void; get: () => number; + migrate: () => void; }; -export const createClientUatCookie = (publishableKey: string, legacy = true): ClientUatCookieHandler => { +export const createClientUatCookie = (publishableKey: string, withSuffix = false): ClientUatCookieHandler => { const suffix = publishableKey.split('_').pop(); - const legacyClientUatCookie = createCookieHandler(CLIENT_UAT_COOKIE_NAME); - const multipleAppsSessionCookie = createCookieHandler(`${CLIENT_UAT_COOKIE_NAME}_${suffix}`); + const clientUatCookieLegacy = createCookieHandler(CLIENT_UAT_COOKIE_NAME); + const clientUatCookieWithSuffix = createCookieHandler(`${CLIENT_UAT_COOKIE_NAME}_${suffix}`); - const clientUatCookie = legacy ? legacyClientUatCookie : multipleAppsSessionCookie; + const clientUatCookie = withSuffix ? clientUatCookieWithSuffix : clientUatCookieLegacy; const get = (): number => { return parseInt(clientUatCookie.get() || '0', 10); }; const set = (client: ClientResource | undefined) => { - legacyClientUatCookie.remove(); - const expires = addYears(Date.now(), 1); const sameSite = inCrossOriginIframe() ? 'None' : 'Strict'; const secure = window.location.protocol === 'https:'; @@ -44,8 +43,19 @@ export const createClientUatCookie = (publishableKey: string, legacy = true): Cl }); }; + const migrate = () => { + if (!withSuffix || clientUatCookieWithSuffix.get()) return; + + const legacyValue = clientUatCookieLegacy.get(); + if (!legacyValue) return; + + clientUatCookieWithSuffix.set(legacyValue); + clientUatCookieLegacy.remove(); + }; + return { set, get, + migrate, }; }; diff --git a/packages/clerk-js/src/core/auth/cookies/devBrowser.ts b/packages/clerk-js/src/core/auth/cookies/devBrowser.ts index 2f9b7315253..ba9f0bb1940 100644 --- a/packages/clerk-js/src/core/auth/cookies/devBrowser.ts +++ b/packages/clerk-js/src/core/auth/cookies/devBrowser.ts @@ -8,21 +8,20 @@ export type DevBrowserCookieHandler = { set: (jwt: string) => void; get: () => string | undefined; remove: () => void; + migrate: () => void; }; -export const createDevBrowserCookie = (publishableKey: string, legacy = true): DevBrowserCookieHandler => { +export const createDevBrowserCookie = (publishableKey: string, withSuffix = false): DevBrowserCookieHandler => { const suffix = publishableKey.split('_').pop(); - const legacyDevBrowserCookie = createCookieHandler(DEV_BROWSER_JWT_KEY); - const multipleAppsDevBrowserCookie = createCookieHandler(`${DEV_BROWSER_JWT_KEY}_${suffix}`); + const devBrowserCookieLegacy = createCookieHandler(DEV_BROWSER_JWT_KEY); + const devBrowserCookieWithSuffix = createCookieHandler(`${DEV_BROWSER_JWT_KEY}_${suffix}`); - const devBrowserCookie = legacy ? legacyDevBrowserCookie : multipleAppsDevBrowserCookie; + const devBrowserCookie = withSuffix ? devBrowserCookieWithSuffix : devBrowserCookieLegacy; const get = () => devBrowserCookie.get(); const set = (jwt: string) => { - legacyDevBrowserCookie.remove(); - const expires = addYears(Date.now(), 1); const sameSite = inCrossOriginIframe() ? 'None' : 'Lax'; const secure = window.location.protocol === 'https:'; @@ -34,8 +33,18 @@ export const createDevBrowserCookie = (publishableKey: string, legacy = true): D }); }; + const migrate = () => { + if (!withSuffix || devBrowserCookieWithSuffix.get()) return; + + const legacyValue = devBrowserCookieLegacy.get(); + if (!legacyValue) return; + + devBrowserCookieWithSuffix.set(legacyValue); + devBrowserCookieLegacy.remove(); + }; + const remove = () => { - legacyDevBrowserCookie.remove(); + devBrowserCookieLegacy.remove(); devBrowserCookie.remove(); }; @@ -43,5 +52,6 @@ export const createDevBrowserCookie = (publishableKey: string, legacy = true): D get, set, remove, + migrate, }; }; diff --git a/packages/clerk-js/src/core/auth/cookies/session.ts b/packages/clerk-js/src/core/auth/cookies/session.ts index 012e320072c..cdeff1e8cd1 100644 --- a/packages/clerk-js/src/core/auth/cookies/session.ts +++ b/packages/clerk-js/src/core/auth/cookies/session.ts @@ -8,6 +8,7 @@ const SESSION_COOKIE_NAME = '__session'; export type SessionCookieHandler = { set: (token: string) => void; remove: () => void; + migrate: () => void; }; /** @@ -15,21 +16,18 @@ export type SessionCookieHandler = { * This is a short-lived JS cookie used to store the current user JWT. * */ -export const createSessionCookie = (publishableKey: string, legacy = true): SessionCookieHandler => { +export const createSessionCookie = (publishableKey: string, withSuffix = false): SessionCookieHandler => { const suffix = publishableKey.split('_').pop(); - const legacySessionCookie = createCookieHandler(SESSION_COOKIE_NAME); - const multipleAppsSessionCookie = createCookieHandler(`${SESSION_COOKIE_NAME}_${suffix}`); + const sessionCookieLegacy = createCookieHandler(SESSION_COOKIE_NAME); + const sessionCookieWithSuffix = createCookieHandler(`${SESSION_COOKIE_NAME}_${suffix}`); - const sessionCookie = legacy ? legacySessionCookie : multipleAppsSessionCookie; + const sessionCookie = withSuffix ? sessionCookieWithSuffix : sessionCookieLegacy; const remove = () => { - legacySessionCookie.remove(); sessionCookie.remove(); }; const set = (token: string) => { - legacySessionCookie.remove(); - const expires = addYears(Date.now(), 1); const sameSite = inCrossOriginIframe() ? 'None' : 'Lax'; const secure = window.location.protocol === 'https:'; @@ -41,8 +39,19 @@ export const createSessionCookie = (publishableKey: string, legacy = true): Sess }); }; + const migrate = () => { + if (!withSuffix || sessionCookieWithSuffix.get()) return; + + const legacyValue = sessionCookieLegacy.get(); + if (!legacyValue) return; + + sessionCookieWithSuffix.set(legacyValue); + sessionCookieLegacy.remove(); + }; + return { set, remove, + migrate, }; }; diff --git a/packages/clerk-js/src/core/auth/devBrowser.ts b/packages/clerk-js/src/core/auth/devBrowser.ts index bf5ee58c183..bceca9fc6cc 100644 --- a/packages/clerk-js/src/core/auth/devBrowser.ts +++ b/packages/clerk-js/src/core/auth/devBrowser.ts @@ -17,6 +17,8 @@ export interface DevBrowser { setDevBrowserJWT(jwt: string): void; removeDevBrowserJWT(): void; + + migrate(): void; } export type CreateDevBrowserOptions = { @@ -25,8 +27,11 @@ export type CreateDevBrowserOptions = { fapiClient: FapiClient; }; -export function createDevBrowser({ publishableKey, frontendApi, fapiClient }: CreateDevBrowserOptions): DevBrowser { - const devBrowserCookie = createDevBrowserCookie(publishableKey); +export function createDevBrowser( + { publishableKey, frontendApi, fapiClient }: CreateDevBrowserOptions, + withSuffix = false, +): DevBrowser { + const devBrowserCookie = createDevBrowserCookie(publishableKey, withSuffix); function getDevBrowserJWT() { return devBrowserCookie.get(); @@ -99,11 +104,16 @@ export function createDevBrowser({ publishableKey, frontendApi, fapiClient }: Cr setDevBrowserJWT(data?.token); } + function migrate() { + devBrowserCookie.migrate(); + } + return { clear, setup, getDevBrowserJWT, setDevBrowserJWT, removeDevBrowserJWT, + migrate, }; } diff --git a/packages/clerk-js/src/core/auth/cookies/safeLock.ts b/packages/clerk-js/src/core/auth/safeLock.ts similarity index 100% rename from packages/clerk-js/src/core/auth/cookies/safeLock.ts rename to packages/clerk-js/src/core/auth/safeLock.ts From f62ef46a824256cd01c0bd5a4b9a328c9fc2e79b Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Wed, 10 Apr 2024 20:09:17 +0300 Subject: [PATCH 6/8] fix(backend): Consider handshake token to be cookie suffixed --- packages/backend/src/tokens/authenticateContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/tokens/authenticateContext.ts b/packages/backend/src/tokens/authenticateContext.ts index c44712c824e..0ae268afaa0 100644 --- a/packages/backend/src/tokens/authenticateContext.ts +++ b/packages/backend/src/tokens/authenticateContext.ts @@ -55,7 +55,7 @@ class AuthenticateContext { this.devBrowserToken = this.getQueryParam(constants.Cookies.DevBrowser) || this.getMultipleAppsCookie(constants.Cookies.DevBrowser); this.handshakeToken = - this.getQueryParam(constants.Cookies.Handshake) || this.getCookie(constants.Cookies.Handshake); + this.getQueryParam(constants.Cookies.Handshake) || this.getMultipleAppsCookie(constants.Cookies.Handshake); } private initHeaderValues() { From fcadc2811aa426c8e0168961b3cc28e50f89e582 Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Wed, 10 Apr 2024 20:13:03 +0300 Subject: [PATCH 7/8] feat(backend): Request handshake to contain suffixed cookies --- packages/backend/src/tokens/request.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index a94d42028d9..aab3abd13df 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -90,6 +90,7 @@ export async function authenticateRequest( const url = new URL(`https://${frontendApiNoProtocol}/v1/client/handshake`); url.searchParams.append('redirect_url', redirectUrl?.href || ''); + url.searchParams.append('_multiple_apps_same_domain', 'true'); if (pk?.instanceType === 'development' && authenticateContext.devBrowserToken) { url.searchParams.append(constants.QueryParameters.DevBrowser, authenticateContext.devBrowserToken); @@ -110,7 +111,7 @@ export async function authenticateRequest( let sessionToken = ''; cookiesToSet.forEach((x: string) => { headers.append('Set-Cookie', x); - if (x.startsWith(`${constants.Cookies.Session}=`)) { + if (x.startsWith(`${constants.Cookies.Session}`)) { sessionToken = x.split(';')[0].substring(10); } }); From 2bf01fe09796db266be79145ae1422ee920c2cef Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Wed, 10 Apr 2024 20:17:35 +0300 Subject: [PATCH 8/8] feat(clerk-js,nextjs): Introduce multipleAppsSameDomain Clerk option to support cookie suffixing --- .../src/core/auth/SessionCookieService.ts | 26 +++++-------------- packages/clerk-js/src/core/auth/devBrowser.ts | 1 + packages/clerk-js/src/core/clerk.ts | 2 +- packages/nextjs/src/pages/ClerkProvider.tsx | 6 ++++- .../src/utils/mergeNextClerkPropsWithEnv.ts | 1 + packages/types/src/clerk.ts | 1 + 6 files changed, 16 insertions(+), 21 deletions(-) diff --git a/packages/clerk-js/src/core/auth/SessionCookieService.ts b/packages/clerk-js/src/core/auth/SessionCookieService.ts index 5c9fb7c545c..378827082a8 100644 --- a/packages/clerk-js/src/core/auth/SessionCookieService.ts +++ b/packages/clerk-js/src/core/auth/SessionCookieService.ts @@ -1,8 +1,7 @@ import { setDevBrowserJWTInURL } from '@clerk/shared/devBrowser'; import { is4xxError, isClerkAPIResponseError, isNetworkError } from '@clerk/shared/error'; -import type { Clerk, EnvironmentResource, TokenResource } from '@clerk/types'; +import type { Clerk, EnvironmentResource } from '@clerk/types'; -import { inBrowser } from '../../utils'; import { clerkCoreErrorTokenRefreshFailed, clerkMissingDevBrowserJwt } from '../errors'; import { eventBus, events } from '../events'; import type { FapiClient } from '../fapiClient'; @@ -14,6 +13,8 @@ import type { DevBrowser } from './devBrowser'; import { createDevBrowser } from './devBrowser'; import { SessionCookiePoller } from './SessionCookiePoller'; +// TODO: make SessionCookieService singleton since it handles updating cookies using a poller +// and we need to avoid updating them concurrently. export class SessionCookieService { private environment: EnvironmentResource | undefined; private poller: SessionCookiePoller | null = null; @@ -24,7 +25,7 @@ export class SessionCookieService { constructor(private clerk: Clerk, fapiClient: FapiClient, multipleAppsSameDomainEnabled = false) { // set cookie on token update eventBus.on(events.TokenUpdate, ({ token }) => { - this.updateSessionCookie(token?.getRawString()); + this.updateSessionCookie(token && token.getRawString()); this.setClientUatCookieForDevelopmentInstances(); }); @@ -90,10 +91,6 @@ export class SessionCookieService { } private refreshTokenOnVisibilityChange() { - if (!inBrowser()) { - return; - } - document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { void this.refreshSessionToken(); @@ -102,28 +99,19 @@ export class SessionCookieService { } private async refreshSessionToken(): Promise { - if (!inBrowser()) { - return; - } - if (!this.clerk.session) { return; } try { - this.updateSessionCookie(await this.clerk.session?.getToken()); + await this.clerk.session.getToken(); } catch (e) { return this.handleGetTokenError(e); } } - private updateSessionCookie(token: TokenResource | string | undefined | null) { - const rawToken = typeof token === 'string' ? token : token?.getRawString(); - - if (rawToken) { - return this.sessionCookie.set(rawToken); - } - return this.sessionCookie.remove(); + private updateSessionCookie(token: string | null) { + return token ? this.sessionCookie.set(token) : this.sessionCookie.remove(); } private setClientUatCookieForDevelopmentInstances() { diff --git a/packages/clerk-js/src/core/auth/devBrowser.ts b/packages/clerk-js/src/core/auth/devBrowser.ts index bceca9fc6cc..fd756122acd 100644 --- a/packages/clerk-js/src/core/auth/devBrowser.ts +++ b/packages/clerk-js/src/core/auth/devBrowser.ts @@ -60,6 +60,7 @@ export function createDevBrowser( if (devBrowserJWT && request?.url) { request.url = setDevBrowserJWTInURL(request.url, devBrowserJWT); } + request.url?.searchParams.append('_multiple_apps_same_domain', withSuffix.toString()); }); fapiClient.onAfterResponse((_, response) => { diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 628473651ec..f13ffa486f4 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1394,7 +1394,7 @@ export class Clerk implements ClerkInterface { }; #loadInStandardBrowser = async (): Promise => { - this.#authService = new SessionCookieService(this, this.#fapiClient); + this.#authService = new SessionCookieService(this, this.#fapiClient, this.#options.multipleAppsSameDomain); /** * 1. Create the devBrowser. diff --git a/packages/nextjs/src/pages/ClerkProvider.tsx b/packages/nextjs/src/pages/ClerkProvider.tsx index d8624455162..1d753e50ce7 100644 --- a/packages/nextjs/src/pages/ClerkProvider.tsx +++ b/packages/nextjs/src/pages/ClerkProvider.tsx @@ -33,7 +33,11 @@ export function ClerkProvider({ children, ...props }: NextClerkProviderProps): J const navigate = (to: string) => push(to); const replaceNavigate = (to: string) => replace(to); - const mergedProps = mergeNextClerkPropsWithEnv({ ...props, routerPush: navigate, routerReplace: replaceNavigate }); + const mergedProps = mergeNextClerkPropsWithEnv({ + ...props, + routerPush: navigate, + routerReplace: replaceNavigate, + }); // ClerkProvider automatically injects __clerk_ssr_state // getAuth returns a user-facing authServerSideProps that hides __clerk_ssr_state // @ts-expect-error initialState is hidden from the types as it's a private prop diff --git a/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts b/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts index 54d06727102..80fbdebe69c 100644 --- a/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts +++ b/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts @@ -22,5 +22,6 @@ export const mergeNextClerkPropsWithEnv = (props: Omit