From 5f1930b624bf627ec7a21ae3da9e2f0ff3c288e1 Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Fri, 10 Nov 2023 15:07:00 +0200 Subject: [PATCH 1/5] chore(backend): Drop unused `RequiredVerifyTokenOptions` type --- packages/backend/src/tokens/index.ts | 2 +- packages/backend/src/tokens/request.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/backend/src/tokens/index.ts b/packages/backend/src/tokens/index.ts index d2a3cbf3c7f..45963f9f6c5 100644 --- a/packages/backend/src/tokens/index.ts +++ b/packages/backend/src/tokens/index.ts @@ -5,4 +5,4 @@ export * from './errors'; export * from './factory'; export { loadInterstitialFromLocal } from './interstitial'; export { debugRequestState } from './request'; -export type { AuthenticateRequestOptions, OptionalVerifyTokenOptions, RequiredVerifyTokenOptions } from './request'; +export type { AuthenticateRequestOptions, OptionalVerifyTokenOptions } from './request'; diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index 3fbb2bbe369..3953aa3fc90 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -28,8 +28,6 @@ export type LoadResourcesOptions = { loadOrganization?: boolean; }; -export type RequiredVerifyTokenOptions = Required>; - export type OptionalVerifyTokenOptions = Partial< Pick< VerifyTokenOptions, From 1119b0fa2f11135f7d6e7d6204c4d21a93041f6d Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Mon, 13 Nov 2023 14:42:50 +0200 Subject: [PATCH 2/5] chore(backend): Restructure AuthStatus/AuthObject/InterstitialRule types dependencies Instead of passing the `AuthenticateRequestOptions` down the dependency tree `authenticateRequest => interstitialRule => AuthStatus => AuthObjects`, we change the approach to bottom-up by definiting types per layer and importing them to the parent layer. Also options of parent layers extend the options of child layers. --- packages/backend/src/tokens/authObjects.ts | 10 +- packages/backend/src/tokens/authStatus.ts | 110 ++++++++++++++---- .../backend/src/tokens/interstitialRule.ts | 42 +++++-- packages/backend/src/tokens/request.ts | 16 +-- packages/nextjs/src/server/getAuth.ts | 2 +- 5 files changed, 130 insertions(+), 50 deletions(-) diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index 49b274f7422..5fb967be287 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -8,17 +8,15 @@ import type { import type { Organization, Session, User } from '../api'; import { createBackendApiClient } from '../api'; -import type { RequestState } from './authStatus'; -import type { AuthenticateRequestOptions } from './request'; -type AuthObjectDebugData = Partial; +type AuthObjectDebugData = Record; type CreateAuthObjectDebug = (data?: AuthObjectDebugData) => AuthObjectDebug; -type AuthObjectDebug = () => unknown; +type AuthObjectDebug = () => AuthObjectDebugData; export type SignedInAuthObjectOptions = { secretKey?: string; - apiUrl: string; - apiVersion: string; + apiUrl?: string; + apiVersion?: string; token: string; session?: Session; user?: User; diff --git a/packages/backend/src/tokens/authStatus.ts b/packages/backend/src/tokens/authStatus.ts index 1e62dcaf071..6abce491104 100644 --- a/packages/backend/src/tokens/authStatus.ts +++ b/packages/backend/src/tokens/authStatus.ts @@ -1,7 +1,7 @@ import type { JwtPayload } from '@clerk/types'; import { createBackendApiClient } from '../api'; -import type { SignedInAuthObject, SignedOutAuthObject } from './authObjects'; +import type { SignedInAuthObject, SignedInAuthObjectOptions, SignedOutAuthObject } from './authObjects'; import { signedInAuthObject, signedOutAuthObject } from './authObjects'; import type { TokenVerificationErrorReason } from './errors'; @@ -81,25 +81,60 @@ export type AuthReason = AuthErrorReason | TokenVerificationErrorReason; export type RequestState = SignedInState | SignedOutState | InterstitialState | UnknownState; -export async function signedIn(options: T, sessionClaims: JwtPayload): Promise { +export type LoadResourcesOptions = { + loadSession?: boolean; + loadUser?: boolean; + loadOrganization?: boolean; +}; + +type RequestStateParams = { + publishableKey?: string; + domain?: string; + isSatellite?: boolean; + proxyUrl?: string; + searchParams?: URLSearchParams; + signInUrl?: string; + signUpUrl?: string; + afterSignInUrl?: string; + afterSignUpUrl?: string; +}; + +type AuthParams = { + /* Client token cookie value */ + cookieToken?: string; + /* Client uat cookie value */ + clientUat?: string; + /* Client token header value */ + headerToken?: string; +}; + +export type AuthStatusOptionsType = LoadResourcesOptions & + Partial & + RequestStateParams & + AuthParams; + +export async function signedIn( + options: T, + sessionClaims: JwtPayload, +): Promise { const { + publishableKey = '', + proxyUrl = '', + isSatellite = false, + domain = '', + signInUrl = '', + signUpUrl = '', + afterSignInUrl = '', + afterSignUpUrl = '', secretKey, apiUrl, apiVersion, cookieToken, - proxyUrl, - publishableKey, - domain, - isSatellite, headerToken, loadSession, loadUser, loadOrganization, - signInUrl, - signUpUrl, - afterSignInUrl, - afterSignUpUrl, - } = options as any; + } = options; const { sid: sessionId, org_id: orgId, sub: userId } = sessionClaims; @@ -154,10 +189,21 @@ export async function signedIn(options: T, sessionClaims: JwtPayload): Promis toAuth: () => authObject, }; } - -export function signedOut(options: T, reason: AuthReason, message = ''): SignedOutState { - const { publishableKey, proxyUrl, isSatellite, domain, signInUrl, signUpUrl, afterSignInUrl, afterSignUpUrl } = - options as any; +export function signedOut( + options: T, + reason: AuthReason, + message = '', +): SignedOutState { + const { + publishableKey = '', + proxyUrl = '', + isSatellite = false, + domain = '', + signInUrl = '', + signUpUrl = '', + afterSignInUrl = '', + afterSignUpUrl = '', + } = options; return { status: AuthStatus.SignedOut, @@ -178,9 +224,22 @@ export function signedOut(options: T, reason: AuthReason, message = ''): Sign }; } -export function interstitial(options: T, reason: AuthReason, message = ''): InterstitialState { - const { publishableKey, proxyUrl, isSatellite, domain, signInUrl, signUpUrl, afterSignInUrl, afterSignUpUrl } = - options as any; +export function interstitial( + options: T, + reason: AuthReason, + message = '', +): InterstitialState { + const { + publishableKey = '', + proxyUrl = '', + isSatellite = false, + domain = '', + signInUrl = '', + signUpUrl = '', + afterSignInUrl = '', + afterSignUpUrl = '', + } = options; + return { status: AuthStatus.Interstitial, reason, @@ -200,8 +259,18 @@ export function interstitial(options: T, reason: AuthReason, message = ''): I }; } -export function unknownState(options: T, reason: AuthReason, message = ''): UnknownState { - const { publishableKey, isSatellite, domain, signInUrl, signUpUrl, afterSignInUrl, afterSignUpUrl } = options as any; +export function unknownState(options: AuthStatusOptionsType, reason: AuthReason, message = ''): UnknownState { + const { + publishableKey = '', + proxyUrl = '', + isSatellite = false, + domain = '', + signInUrl = '', + signUpUrl = '', + afterSignInUrl = '', + afterSignUpUrl = '', + } = options; + return { status: AuthStatus.Unknown, reason, @@ -209,6 +278,7 @@ export function unknownState(options: T, reason: AuthReason, message = ''): U publishableKey, isSatellite, domain, + proxyUrl, signInUrl, signUpUrl, afterSignInUrl, diff --git a/packages/backend/src/tokens/interstitialRule.ts b/packages/backend/src/tokens/interstitialRule.ts index 86e216b6af0..cd793a258fd 100644 --- a/packages/backend/src/tokens/interstitialRule.ts +++ b/packages/backend/src/tokens/interstitialRule.ts @@ -1,14 +1,32 @@ import { checkCrossOrigin } from '../util/request'; import { isDevelopmentFromApiKey, isProductionFromApiKey } from '../util/shared'; -import type { RequestState } from './authStatus'; +import type { AuthStatusOptionsType, RequestState } from './authStatus'; import { AuthErrorReason, interstitial, signedIn, signedOut } from './authStatus'; -import type { AuthenticateRequestOptions } from './request'; import { verifyToken } from './verify'; +type InterstitialRuleOptions = AuthStatusOptionsType & { + /* Request origin header value */ + origin?: string; + /* Request host header value */ + host?: string; + /* Request forwarded host value */ + forwardedHost?: string; + /* Request forwarded proto value */ + forwardedProto?: string; + /* Request referrer */ + referrer?: string; + /* Request user-agent value */ + userAgent?: string; + /* Client token cookie value */ + cookieToken?: string; + /* Client uat cookie value */ + clientUat?: string; + /* Client token header value */ + headerToken?: string; +}; + type InterstitialRuleResult = RequestState | undefined; -type InterstitialRule = ( - opts: T, -) => Promise | InterstitialRuleResult; +type InterstitialRule = (opts: InterstitialRuleOptions) => Promise | InterstitialRuleResult; const shouldRedirectToSatelliteUrl = (qp?: URLSearchParams) => !!qp?.get('__clerk_satellite_url'); const hasJustSynced = (qp?: URLSearchParams) => qp?.get('__clerk_synced') === 'true'; @@ -116,18 +134,18 @@ export const hasPositiveClientUatButCookieIsMissing: InterstitialRule = options }; export const hasValidHeaderToken: InterstitialRule = async options => { - const { headerToken } = options as any; - const sessionClaims = await verifyRequestState(options, headerToken); + const { headerToken } = options; + const sessionClaims = await verifyRequestState(options, headerToken as string); return await signedIn(options, sessionClaims); }; export const hasValidCookieToken: InterstitialRule = async options => { - const { cookieToken, clientUat } = options as any; - const sessionClaims = await verifyRequestState(options, cookieToken); + const { cookieToken, clientUat } = options; + const sessionClaims = await verifyRequestState(options, cookieToken as string); const state = await signedIn(options, sessionClaims); const jwt = state.toAuth().sessionClaims; - const cookieTokenIsOutdated = jwt.iat < Number.parseInt(clientUat); + const cookieTokenIsOutdated = jwt.iat < Number.parseInt(clientUat as string); if (!clientUat || cookieTokenIsOutdated) { return interstitial(options, AuthErrorReason.CookieOutDated); @@ -136,7 +154,7 @@ export const hasValidCookieToken: InterstitialRule = async options => { return state; }; -export async function runInterstitialRules( +export async function runInterstitialRules( opts: T, rules: InterstitialRule[], ): Promise { @@ -150,7 +168,7 @@ export async function runInterstitialRules return signedOut(opts, AuthErrorReason.UnexpectedError); } -async function verifyRequestState(options: AuthenticateRequestOptions, token: string) { +async function verifyRequestState(options: InterstitialRuleOptions, token: string) { const { isSatellite, proxyUrl } = options; let issuer; if (isSatellite) { diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index 3953aa3fc90..f90c1ac2bc6 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -2,7 +2,7 @@ import { API_URL, API_VERSION, constants } from '../constants'; import { assertValidSecretKey } from '../util/assertValidSecretKey'; import { buildRequest, stripAuthorizationHeader } from '../util/IsomorphicRequest'; import { isDevelopmentFromApiKey } from '../util/shared'; -import type { RequestState } from './authStatus'; +import type { LoadResourcesOptions, RequestState } from './authStatus'; import { AuthErrorReason, interstitial, signedOut, unknownState } from './authStatus'; import type { TokenCarrier } from './errors'; import { TokenVerificationError, TokenVerificationErrorReason } from './errors'; @@ -22,12 +22,6 @@ import { } from './interstitialRule'; import type { VerifyTokenOptions } from './verify'; -export type LoadResourcesOptions = { - loadSession?: boolean; - loadUser?: boolean; - loadOrganization?: boolean; -}; - export type OptionalVerifyTokenOptions = Partial< Pick< VerifyTokenOptions, @@ -161,13 +155,13 @@ export async function authenticateRequest(options: AuthenticateRequestOptions): if (reasonToReturnInterstitial) { if (tokenCarrier === 'header') { - return unknownState(options, err.reason, err.getFullMessage()); + return unknownState(options, err.reason, err.getFullMessage()); } - return interstitial(options, err.reason, err.getFullMessage()); + return interstitial(options, err.reason, err.getFullMessage()); } - return signedOut(options, err.reason, err.getFullMessage()); + return signedOut(options, err.reason, err.getFullMessage()); } - return signedOut(options, AuthErrorReason.UnexpectedError, (err as Error).message); + return signedOut(options, AuthErrorReason.UnexpectedError, (err as Error).message); } if (options.headerToken) { diff --git a/packages/nextjs/src/server/getAuth.ts b/packages/nextjs/src/server/getAuth.ts index 773d1d9f7c3..bb13dc840fa 100644 --- a/packages/nextjs/src/server/getAuth.ts +++ b/packages/nextjs/src/server/getAuth.ts @@ -40,7 +40,7 @@ export const createGetAuth = ({ // When the auth status is set, we trust that the middleware has already run // Then, we don't have to re-verify the JWT here, // we can just strip out the claims manually. - const authStatus = getAuthKeyFromRequest(req, 'AuthStatus'); + const authStatus = getAuthKeyFromRequest(req, 'AuthStatus') as AuthStatus; const authMessage = getAuthKeyFromRequest(req, 'AuthMessage'); const authReason = getAuthKeyFromRequest(req, 'AuthReason'); logger.debug('Headers debug', { authStatus, authMessage, authReason }); From 391701a2921ff4336b27961f7bced159c057ecaf Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Mon, 13 Nov 2023 15:17:15 +0200 Subject: [PATCH 3/5] feat(backend): Enforce `request` param in authenticateRequest - replace header related options --- packages/backend/src/api/factory.ts | 11 +- packages/backend/src/api/request.ts | 14 +- packages/backend/src/tokens/authObjects.ts | 7 +- packages/backend/src/tokens/authStatus.ts | 1 - packages/backend/src/tokens/factory.ts | 6 - .../backend/src/tokens/interstitialRule.ts | 4 +- packages/backend/src/tokens/request.test.ts | 266 ++++++++++-------- packages/backend/src/tokens/request.ts | 122 +++----- 8 files changed, 218 insertions(+), 213 deletions(-) diff --git a/packages/backend/src/api/factory.ts b/packages/backend/src/api/factory.ts index 1b149c71857..e187b82ebfb 100644 --- a/packages/backend/src/api/factory.ts +++ b/packages/backend/src/api/factory.ts @@ -16,16 +16,7 @@ import { } from './endpoints'; import { buildRequest } from './request'; -export type CreateBackendApiOptions = { - /* Secret Key */ - secretKey?: string; - /* Backend API URL */ - apiUrl?: string; - /* Backend API version */ - apiVersion?: string; - /* Library/SDK name */ - userAgent?: string; -}; +export type CreateBackendApiOptions = Parameters[0]; export type ApiClient = ReturnType; diff --git a/packages/backend/src/api/request.ts b/packages/backend/src/api/request.ts index 0b8be4153e2..f7c566a0a1d 100644 --- a/packages/backend/src/api/request.ts +++ b/packages/backend/src/api/request.ts @@ -8,7 +8,6 @@ import { API_URL, API_VERSION, constants, USER_AGENT } from '../constants'; import runtime from '../runtime'; import { assertValidSecretKey } from '../util/assertValidSecretKey'; import { joinPaths } from '../util/path'; -import type { CreateBackendApiOptions } from './factory'; import { deserialize } from './resources/Deserializer'; export type ClerkBackendApiRequestOptions = { @@ -64,7 +63,18 @@ const withLegacyReturn = } }; -export function buildRequest(options: CreateBackendApiOptions) { +type BuildRequestOptions = { + /* Secret Key */ + secretKey?: string; + /* Backend API URL */ + apiUrl?: string; + /* Backend API version */ + apiVersion?: string; + /* Library/SDK name */ + userAgent?: string; +}; + +export function buildRequest(options: BuildRequestOptions) { const request = async (requestOptions: ClerkBackendApiRequestOptions): Promise> => { const { secretKey, apiUrl = API_URL, apiVersion = API_VERSION, userAgent = USER_AGENT } = options; const { path, method, queryParams, headerParams, bodyParams, formData } = requestOptions; diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index 5fb967be287..54e57809f8e 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -6,17 +6,14 @@ import type { ServerGetTokenOptions, } from '@clerk/types'; -import type { Organization, Session, User } from '../api'; +import type { CreateBackendApiOptions, Organization, Session, User } from '../api'; import { createBackendApiClient } from '../api'; type AuthObjectDebugData = Record; type CreateAuthObjectDebug = (data?: AuthObjectDebugData) => AuthObjectDebug; type AuthObjectDebug = () => AuthObjectDebugData; -export type SignedInAuthObjectOptions = { - secretKey?: string; - apiUrl?: string; - apiVersion?: string; +export type SignedInAuthObjectOptions = CreateBackendApiOptions & { token: string; session?: Session; user?: User; diff --git a/packages/backend/src/tokens/authStatus.ts b/packages/backend/src/tokens/authStatus.ts index 6abce491104..b39c53fa540 100644 --- a/packages/backend/src/tokens/authStatus.ts +++ b/packages/backend/src/tokens/authStatus.ts @@ -92,7 +92,6 @@ type RequestStateParams = { domain?: string; isSatellite?: boolean; proxyUrl?: string; - searchParams?: URLSearchParams; signInUrl?: string; signUpUrl?: string; afterSignInUrl?: string; diff --git a/packages/backend/src/tokens/factory.ts b/packages/backend/src/tokens/factory.ts index 7951457d4a1..7fed4f84b27 100644 --- a/packages/backend/src/tokens/factory.ts +++ b/packages/backend/src/tokens/factory.ts @@ -18,7 +18,6 @@ export type CreateAuthenticateRequestOptions = { | 'proxyUrl' | 'domain' | 'isSatellite' - | 'userAgent' > >; apiClient: ApiClient; @@ -36,7 +35,6 @@ export function createAuthenticateRequest(params: CreateAuthenticateRequestOptio isSatellite: buildtimeIsSatellite = false, domain: buildtimeDomain = '', audience: buildtimeAudience = '', - userAgent: buildtimeUserAgent, } = params.options; const authenticateRequest = ({ @@ -47,8 +45,6 @@ export function createAuthenticateRequest(params: CreateAuthenticateRequestOptio jwtKey: runtimeJwtKey, isSatellite: runtimeIsSatellite, domain: runtimeDomain, - searchParams: runtimeSearchParams, - userAgent: runtimeUserAgent, ...rest }: Omit) => { return authenticateRequestOriginal({ @@ -62,8 +58,6 @@ export function createAuthenticateRequest(params: CreateAuthenticateRequestOptio isSatellite: runtimeIsSatellite || buildtimeIsSatellite, domain: runtimeDomain || buildtimeDomain, jwtKey: runtimeJwtKey || buildtimeJwtKey, - searchParams: runtimeSearchParams, - userAgent: runtimeUserAgent?.toString() || buildtimeUserAgent, }); }; diff --git a/packages/backend/src/tokens/interstitialRule.ts b/packages/backend/src/tokens/interstitialRule.ts index cd793a258fd..0593bdc5342 100644 --- a/packages/backend/src/tokens/interstitialRule.ts +++ b/packages/backend/src/tokens/interstitialRule.ts @@ -4,7 +4,7 @@ import type { AuthStatusOptionsType, RequestState } from './authStatus'; import { AuthErrorReason, interstitial, signedIn, signedOut } from './authStatus'; import { verifyToken } from './verify'; -type InterstitialRuleOptions = AuthStatusOptionsType & { +export type InterstitialRuleOptions = AuthStatusOptionsType & { /* Request origin header value */ origin?: string; /* Request host header value */ @@ -23,6 +23,8 @@ type InterstitialRuleOptions = AuthStatusOptionsType & { clientUat?: string; /* Client token header value */ headerToken?: string; + /* Request search params value */ + searchParams?: URLSearchParams; }; type InterstitialRuleResult = RequestState | undefined; diff --git a/packages/backend/src/tokens/request.test.ts b/packages/backend/src/tokens/request.test.ts index 4738139cc87..21c5255883e 100644 --- a/packages/backend/src/tokens/request.test.ts +++ b/packages/backend/src/tokens/request.test.ts @@ -144,24 +144,55 @@ function assertSignedIn( export default (QUnit: QUnit) => { const { module, test, skip } = QUnit; - /* An otherwise bare state on a request. */ - const defaultMockAuthenticateRequestOptions = { - secretKey: 'deadbeef', - apiUrl: 'https://api.clerk.test', - apiVersion: 'v1', - publishableKey: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA', - proxyUrl: '', + const defaultHeaders: Record = { host: 'example.com', - userAgent: 'Mozilla/TestAgent', - skipJwksCache: true, - isSatellite: false, - signInUrl: '', - signUpUrl: '', - afterSignInUrl: '', - afterSignUpUrl: '', - domain: '', - searchParams: new URLSearchParams(), - } satisfies AuthenticateRequestOptions; + 'user-agent': 'Mozilla/TestAgent', + }; + + /* An otherwise bare state on a request. */ + const defaultMockAuthenticateRequestOptions = (headers = defaultHeaders, requestUrl = 'http://clerk.com/path') => + ({ + secretKey: 'deadbeef', + apiUrl: 'https://api.clerk.test', + apiVersion: 'v1', + publishableKey: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA', + proxyUrl: '', + skipJwksCache: true, + isSatellite: false, + signInUrl: '', + signUpUrl: '', + afterSignInUrl: '', + afterSignUpUrl: '', + domain: '', + request: new Request(requestUrl, { headers }), + } satisfies AuthenticateRequestOptions); + + const defaultMockHeaderAuthOptions = (headers = defaultHeaders, requestUrl?) => { + return { + ...defaultMockAuthenticateRequestOptions( + { + authorization: mockJwt, + ...headers, + }, + requestUrl, + ), + }; + }; + + const defaultMockCookieAuthOptions = (headers = defaultHeaders, cookies = {}, requestUrl?) => { + const cookieStr = Object.entries(cookies) + .map(([k, v]) => `${k}=${v}`) + .join(';'); + return { + ...defaultMockAuthenticateRequestOptions( + { + cookie: cookieStr, + ...headers, + }, + requestUrl, + ), + }; + }; module('tokens.authenticateRequest(options)', hooks => { let fakeClock; @@ -186,8 +217,7 @@ export default (QUnit: QUnit) => { test('returns signed out state if jwk fails to load from remote', async assert => { fakeFetch.onCall(0).returns(jsonOk({})); const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, - headerToken: mockJwt, + ...defaultMockHeaderAuthOptions(), skipJwksCache: false, }); @@ -201,10 +231,7 @@ export default (QUnit: QUnit) => { }); test('headerToken: returns signed in state when a valid token [1y.2y]', async assert => { - const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, - headerToken: mockJwt, - }); + const requestState = await authenticateRequest(defaultMockHeaderAuthOptions()); assertSignedIn(assert, requestState); assertSignedInToAuth(assert, requestState); @@ -219,8 +246,7 @@ export default (QUnit: QUnit) => { test('headerToken: returns signed out state when a token with invalid authorizedParties [1y.2n]', async assert => { const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, - headerToken: mockJwt, + ...defaultMockHeaderAuthOptions(), authorizedParties: ['whatever'], }); @@ -237,20 +263,19 @@ export default (QUnit: QUnit) => { // advance clock for 1 hour fakeClock.tick(3600 * 1000); - const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, - headerToken: mockJwt, - }); + const requestState = await authenticateRequest(defaultMockHeaderAuthOptions()); assertUnknown(assert, requestState, TokenVerificationErrorReason.TokenExpired); assert.strictEqual(requestState.toAuth(), null); }); test('headerToken: returns signed out state when invalid signature [1y.2n]', async assert => { - const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, - headerToken: mockInvalidSignatureJwt, - }); + const requestState = await authenticateRequest( + defaultMockHeaderAuthOptions({ + ...defaultHeaders, + authorization: mockInvalidSignatureJwt, + }), + ); const errMessage = 'JWT signature is invalid. (reason=token-invalid-signature, token-carrier=header)'; assertSignedOut(assert, requestState, { @@ -261,10 +286,12 @@ export default (QUnit: QUnit) => { }); test('headerToken: returns signed out state when an malformed token [1y.1n]', async assert => { - const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, - headerToken: 'test_header_token', - }); + const requestState = await authenticateRequest( + defaultMockHeaderAuthOptions({ + ...defaultHeaders, + authorization: 'test_header_token', + }), + ); const errMessage = 'Invalid JWT form. A JWT consists of three parts separated by dots. (reason=token-invalid, token-carrier=header)'; @@ -280,12 +307,16 @@ export default (QUnit: QUnit) => { // test('cookieToken: returns signed out state when cross-origin request [2y]', async assert => { - const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, - origin: 'https://clerk.com', - forwardedProto: 'http', - cookieToken: mockJwt, - }); + const requestState = await authenticateRequest( + defaultMockCookieAuthOptions( + { + ...defaultHeaders, + origin: 'https://clerk.com', + 'x-forwarded-proto': 'http', + }, + { __session: mockJwt }, + ), + ); assertSignedOut(assert, requestState, { reason: AuthErrorReason.HeaderMissingCORS, @@ -296,11 +327,14 @@ export default (QUnit: QUnit) => { test('cookieToken: returns signed out when non browser requests in development [3y]', async assert => { const nonBrowserUserAgent = 'curl'; const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, + ...defaultMockCookieAuthOptions( + { + ...defaultHeaders, + 'user-agent': nonBrowserUserAgent, + }, + { __client_uat: '12345', __session: mockJwt }, + ), secretKey: 'test_deadbeef', - userAgent: nonBrowserUserAgent, - clientUat: '12345', - cookieToken: mockJwt, }); assertSignedOut(assert, requestState, { reason: AuthErrorReason.HeaderMissingNonBrowser }); @@ -309,7 +343,12 @@ export default (QUnit: QUnit) => { test('cookieToken: returns interstitial when clientUat is missing or equals to 0 and is satellite and not is synced [11y]', async assert => { const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, + ...defaultMockCookieAuthOptions( + { + ...defaultHeaders, + }, + { __client_uat: '0' }, + ), secretKey: 'deadbeef', clientUat: '0', isSatellite: true, @@ -329,13 +368,17 @@ export default (QUnit: QUnit) => { test('cookieToken: returns signed out is satellite but a non-browser request [11y]', async assert => { const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, + ...defaultMockCookieAuthOptions( + { + ...defaultHeaders, + 'user-agent': '[some-agent]', + }, + { __client_uat: '0' }, + ), secretKey: 'deadbeef', - clientUat: '0', isSatellite: true, signInUrl: 'https://primary.dev/sign-in', domain: 'satellite.dev', - userAgent: '[some-agent]', }); assertSignedOut(assert, requestState, { @@ -347,9 +390,9 @@ export default (QUnit: QUnit) => { assertSignedOutToAuth(assert, requestState); }); - test('returns interstitial when app is satellite, returns from primary and is dev instance [13y]', async assert => { + test('cookieToken: returns interstitial when app is satellite, returns from primary and is dev instance [13y]', async assert => { const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, + ...defaultMockCookieAuthOptions(), secretKey: 'sk_test_deadbeef', signInUrl: 'http://primary.example/sign-in', isSatellite: true, @@ -369,13 +412,11 @@ export default (QUnit: QUnit) => { test('cookieToken: returns interstitial when app is not satellite and responds to syncing on dev instances[12y]', async assert => { const sp = new URLSearchParams(); sp.set('__clerk_satellite_url', 'http://localhost:3000'); + const requestUrl = `http://clerk.com/path?${sp.toString()}`; const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, + ...defaultMockCookieAuthOptions(defaultHeaders, { __client_uat: '12345', __session: mockJwt }, requestUrl), secretKey: 'sk_test_deadbeef', - clientUat: '12345', isSatellite: false, - cookieToken: mockJwt, - searchParams: sp, }); assertInterstitial(assert, requestState, { @@ -387,7 +428,7 @@ export default (QUnit: QUnit) => { test('cookieToken: returns signed out when no cookieToken and no clientUat in production [4y]', async assert => { const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, + ...defaultMockCookieAuthOptions(), secretKey: 'live_deadbeef', }); @@ -399,8 +440,7 @@ export default (QUnit: QUnit) => { test('cookieToken: returns interstitial when no clientUat in development [5y]', async assert => { const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, - cookieToken: mockJwt, + ...defaultMockCookieAuthOptions(defaultHeaders, { __session: mockJwt }), secretKey: 'test_deadbeef', }); @@ -412,10 +452,8 @@ export default (QUnit: QUnit) => { // Omit because it caused view-source to always returns the interstitial in development mode (there's no referrer for view-source) skip('cookieToken: returns interstitial when no referrer in development [6y]', async assert => { const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, - cookieToken: mockJwt, + ...defaultMockCookieAuthOptions(defaultHeaders, { __client_uat: '12345', __session: mockJwt }), secretKey: 'test_deadbeef', - clientUat: '12345', }); assertInterstitial(assert, requestState, { reason: AuthErrorReason.CrossOriginReferrer }); @@ -426,11 +464,15 @@ export default (QUnit: QUnit) => { test('cookieToken: returns interstitial when crossOriginReferrer in development [6y]', async assert => { // Scenario: after auth action on Clerk-hosted UIs const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, - cookieToken: mockJwt, + ...defaultMockCookieAuthOptions( + { + ...defaultHeaders, + // this is not a typo, it's intentional to be `referer` to match HTTP header key + referer: 'https://clerk.com', + }, + { __client_uat: '12345', __session: mockJwt }, + ), secretKey: 'test_deadbeef', - clientUat: '12345', - referrer: 'https://clerk.com', }); assertInterstitial(assert, requestState, { reason: AuthErrorReason.CrossOriginReferrer }); @@ -441,11 +483,15 @@ export default (QUnit: QUnit) => { test('cookieToken: returns undefined when crossOriginReferrer in development and is satellite [6n]', async assert => { // Scenario: after auth action on Clerk-hosted UIs const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, - cookieToken: mockJwt, + ...defaultMockCookieAuthOptions( + { + ...defaultHeaders, + // this is not a typo, it's intentional to be `referer` to match HTTP header key + referer: 'https://clerk.com', + }, + { __client_uat: '12345', __session: mockJwt }, + ), secretKey: 'pk_test_deadbeef', - clientUat: '12345', - referrer: 'https://clerk.com', isSatellite: true, signInUrl: 'https://localhost:3000/sign-in/', domain: 'localhost:3001', @@ -468,9 +514,8 @@ export default (QUnit: QUnit) => { test('cookieToken: returns interstitial when clientUat > 0 and no cookieToken [8y]', async assert => { const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, + ...defaultMockCookieAuthOptions(defaultHeaders, { __client_uat: '12345' }), secretKey: 'deadbeef', - clientUat: '1234', }); assertInterstitial(assert, requestState, { reason: AuthErrorReason.CookieMissing }); @@ -480,8 +525,7 @@ export default (QUnit: QUnit) => { test('cookieToken: returns signed out when clientUat = 0 and no cookieToken [9y]', async assert => { const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, - clientUat: '0', + ...defaultMockCookieAuthOptions(defaultHeaders, { __client_uat: '0' }), }); assertSignedOut(assert, requestState, { @@ -491,11 +535,12 @@ export default (QUnit: QUnit) => { }); test('cookieToken: returns interstitial when clientUat > cookieToken.iat [10n]', async assert => { - const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, - cookieToken: mockJwt, - clientUat: `${mockJwtPayload.iat + 10}`, - }); + const requestState = await authenticateRequest( + defaultMockCookieAuthOptions(defaultHeaders, { + __client_uat: `${mockJwtPayload.iat + 10}`, + __session: mockJwt, + }), + ); assertInterstitial(assert, requestState, { reason: AuthErrorReason.CookieOutDated }); assert.equal(requestState.message, ''); @@ -503,11 +548,12 @@ export default (QUnit: QUnit) => { }); test('cookieToken: returns signed out when cookieToken.iat >= clientUat and malformed token [10y.1n]', async assert => { - const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, - cookieToken: mockMalformedJwt, - clientUat: `${mockJwtPayload.iat - 10}`, - }); + const requestState = await authenticateRequest( + defaultMockCookieAuthOptions(defaultHeaders, { + __client_uat: `${mockJwtPayload.iat - 10}`, + __session: mockMalformedJwt, + }), + ); const errMessage = 'Subject claim (sub) is required and must be a string. Received undefined. Make sure that this is a valid Clerk generate JWT. (reason=token-verification-failed, token-carrier=cookie)'; @@ -519,11 +565,12 @@ export default (QUnit: QUnit) => { }); test('cookieToken: returns signed in when cookieToken.iat >= clientUat and valid token [10y.2y]', async assert => { - const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, - cookieToken: mockJwt, - clientUat: `${mockJwtPayload.iat - 10}`, - }); + const requestState = await authenticateRequest( + defaultMockCookieAuthOptions(defaultHeaders, { + __client_uat: `${mockJwtPayload.iat - 10}`, + __session: mockJwt, + }), + ); assertSignedIn(assert, requestState); assertSignedInToAuth(assert, requestState); @@ -540,11 +587,12 @@ export default (QUnit: QUnit) => { // advance clock for 1 hour fakeClock.tick(3600 * 1000); - const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, - cookieToken: mockJwt, - clientUat: `${mockJwtPayload.iat - 10}`, - }); + const requestState = await authenticateRequest( + defaultMockCookieAuthOptions(defaultHeaders, { + __client_uat: `${mockJwtPayload.iat - 10}`, + __session: mockJwt, + }), + ); assertInterstitial(assert, requestState, { reason: TokenVerificationErrorReason.TokenExpired }); assert.true(/^JWT is expired/.test(requestState.message || '')); @@ -553,11 +601,14 @@ export default (QUnit: QUnit) => { test('cookieToken: returns signed in for Amazon Cloudfront userAgent', async assert => { const requestState = await authenticateRequest({ - ...defaultMockAuthenticateRequestOptions, + ...defaultMockCookieAuthOptions( + { + ...defaultHeaders, + 'user-agent': 'Amazon CloudFront', + }, + { __client_uat: `12345`, __session: mockJwt }, + ), secretKey: 'test_deadbeef', - userAgent: 'Amazon CloudFront', - clientUat: '12345', - cookieToken: mockJwt, }); assertSignedIn(assert, requestState); @@ -577,20 +628,11 @@ export default (QUnit: QUnit) => { userAgent: '', }; - test('returns options even if headers exist', async assert => { - const headers = key => (key === 'x-forwarded-proto' ? 'https' : ''); - const options = { forwardedProto: 'http' }; - assert.propEqual(loadOptionsFromHeaders(options, headers), { - ...defaultOptions, - forwardedProto: 'http', - }); - }); - - test('returns forwarded headers from headers', async assert => { + 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.propEqual(loadOptionsFromHeaders({}, headers), { + assert.propEqual(loadOptionsFromHeaders(headers), { ...defaultOptions, forwardedProto: 'http', forwardedPort: '80', @@ -598,7 +640,7 @@ export default (QUnit: QUnit) => { }); }); - test('returns Cloudfront forwarded proto from headers even if forwarded proto header exists', async assert => { + test('returns Cloudfront forwarded proto from headers even if forwarded proto header exists', assert => { const headersData = { 'cloudfront-forwarded-proto': 'https', 'x-forwarded-proto': 'http', @@ -607,7 +649,7 @@ export default (QUnit: QUnit) => { }; const headers = key => headersData[key] || ''; - assert.propEqual(loadOptionsFromHeaders({}, headers), { + assert.propEqual(loadOptionsFromHeaders(headers), { ...defaultOptions, forwardedProto: 'https', forwardedPort: '80', diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index f90c1ac2bc6..5f37e9e9562 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -1,11 +1,12 @@ -import { API_URL, API_VERSION, constants } from '../constants'; +import { constants } from '../constants'; import { assertValidSecretKey } from '../util/assertValidSecretKey'; import { buildRequest, stripAuthorizationHeader } from '../util/IsomorphicRequest'; import { isDevelopmentFromApiKey } from '../util/shared'; -import type { LoadResourcesOptions, RequestState } from './authStatus'; +import type { AuthStatusOptionsType, LoadResourcesOptions, RequestState } from './authStatus'; import { AuthErrorReason, interstitial, signedOut, unknownState } from './authStatus'; import type { TokenCarrier } from './errors'; import { TokenVerificationError, TokenVerificationErrorReason } from './errors'; +import type { InterstitialRuleOptions } from './interstitialRule'; import { crossOriginRequestWithoutHeader, hasPositiveClientUatButCookieIsMissing, @@ -21,7 +22,6 @@ import { runInterstitialRules, } from './interstitialRule'; import type { VerifyTokenOptions } from './verify'; - export type OptionalVerifyTokenOptions = Partial< Pick< VerifyTokenOptions, @@ -29,42 +29,7 @@ export type OptionalVerifyTokenOptions = Partial< > >; -export type AuthenticateRequestOptions = OptionalVerifyTokenOptions & - LoadResourcesOptions & { - publishableKey?: string; - secretKey?: string; - apiVersion?: string; - apiUrl?: string; - /* Client token cookie value */ - cookieToken?: string; - /* Client uat cookie value */ - clientUat?: string; - /* Client token header value */ - headerToken?: string; - /* Request origin header value */ - origin?: string; - /* Request host header value */ - host?: string; - /* Request forwarded host value */ - forwardedHost?: string; - /* Request forwarded port value */ - forwardedPort?: string; - /* Request forwarded proto value */ - forwardedProto?: string; - /* Request referrer */ - referrer?: string; - /* Request user-agent value */ - userAgent?: string; - domain?: string; - isSatellite?: boolean; - proxyUrl?: string; - searchParams?: URLSearchParams; - signInUrl?: string; - signUpUrl?: string; - afterSignInUrl?: string; - afterSignUpUrl?: string; - request?: Request; - }; +export type AuthenticateRequestOptions = AuthStatusOptionsType & OptionalVerifyTokenOptions & { request: Request }; function assertSignInUrlExists(signInUrl: string | undefined, key: string): asserts signInUrl is string { if (!signInUrl && isDevelopmentFromApiKey(key)) { @@ -94,29 +59,26 @@ function assertSignInUrlFormatAndOrigin(_signInUrl: string, origin: string) { export async function authenticateRequest(options: AuthenticateRequestOptions): Promise { const { cookies, headers, searchParams } = buildRequest(options?.request); - options = { + const ruleOptions = { ...options, - ...loadOptionsFromHeaders(options, headers), - apiUrl: options.apiUrl || API_URL, - apiVersion: options.apiVersion || API_VERSION, - cookieToken: options.cookieToken || cookies?.(constants.Cookies.Session), - clientUat: options.clientUat || cookies?.(constants.Cookies.ClientUat), - searchParams: options.searchParams || searchParams || undefined, - }; + ...loadOptionsFromHeaders(headers), + ...loadOptionsFromCookies(cookies), + searchParams, + } satisfies InterstitialRuleOptions; - assertValidSecretKey(options.secretKey); + assertValidSecretKey(ruleOptions.secretKey); - if (options.isSatellite) { - assertSignInUrlExists(options.signInUrl, options.secretKey); - if (options.signInUrl && options.origin /* could this actually be undefined? */) { - assertSignInUrlFormatAndOrigin(options.signInUrl, options.origin); + if (ruleOptions.isSatellite) { + assertSignInUrlExists(ruleOptions.signInUrl, ruleOptions.secretKey); + if (ruleOptions.signInUrl && ruleOptions.origin) { + assertSignInUrlFormatAndOrigin(ruleOptions.signInUrl, ruleOptions.origin); } - assertProxyUrlOrDomain(options.proxyUrl || options.domain); + assertProxyUrlOrDomain(ruleOptions.proxyUrl || ruleOptions.domain); } async function authenticateRequestWithTokenInHeader() { try { - const state = await runInterstitialRules(options, [hasValidHeaderToken]); + const state = await runInterstitialRules(ruleOptions, [hasValidHeaderToken]); return state; } catch (err) { return handleError(err, 'header'); @@ -125,7 +87,7 @@ export async function authenticateRequest(options: AuthenticateRequestOptions): async function authenticateRequestWithTokenInCookie() { try { - const state = await runInterstitialRules(options, [ + const state = await runInterstitialRules(ruleOptions, [ crossOriginRequestWithoutHeader, nonBrowserRequestInDevRule, isSatelliteAndNeedsSyncing, @@ -155,16 +117,16 @@ export async function authenticateRequest(options: AuthenticateRequestOptions): if (reasonToReturnInterstitial) { if (tokenCarrier === 'header') { - return unknownState(options, err.reason, err.getFullMessage()); + return unknownState(ruleOptions, err.reason, err.getFullMessage()); } - return interstitial(options, err.reason, err.getFullMessage()); + return interstitial(ruleOptions, err.reason, err.getFullMessage()); } - return signedOut(options, err.reason, err.getFullMessage()); + return signedOut(ruleOptions, err.reason, err.getFullMessage()); } - return signedOut(options, AuthErrorReason.UnexpectedError, (err as Error).message); + return signedOut(ruleOptions, AuthErrorReason.UnexpectedError, (err as Error).message); } - if (options.headerToken) { + if (ruleOptions.headerToken) { return authenticateRequestWithTokenInHeader(); } return authenticateRequestWithTokenInCookie(); @@ -178,27 +140,35 @@ export const debugRequestState = (params: RequestState) => { export type DebugRequestSate = ReturnType; /** - * Load authenticate request options from the options provided or fallback to headers. + * Load authenticate request options related to headers. */ -export const loadOptionsFromHeaders = ( - options: AuthenticateRequestOptions, - headers: ReturnType['headers'], -) => { +export const loadOptionsFromHeaders = (headers: ReturnType['headers']) => { if (!headers) { return {}; } return { - headerToken: stripAuthorizationHeader(options.headerToken || headers(constants.Headers.Authorization)), - origin: options.origin || headers(constants.Headers.Origin), - host: options.host || headers(constants.Headers.Host), - forwardedHost: options.forwardedHost || headers(constants.Headers.ForwardedHost), - forwardedPort: options.forwardedPort || headers(constants.Headers.ForwardedPort), - forwardedProto: - options.forwardedProto || - headers(constants.Headers.CloudFrontForwardedProto) || - headers(constants.Headers.ForwardedProto), - referrer: options.referrer || headers(constants.Headers.Referrer), - userAgent: options.userAgent || headers(constants.Headers.UserAgent), + headerToken: stripAuthorizationHeader(headers(constants.Headers.Authorization)), + origin: headers(constants.Headers.Origin), + host: headers(constants.Headers.Host), + forwardedHost: headers(constants.Headers.ForwardedHost), + forwardedPort: headers(constants.Headers.ForwardedPort), + forwardedProto: headers(constants.Headers.CloudFrontForwardedProto) || headers(constants.Headers.ForwardedProto), + referrer: headers(constants.Headers.Referrer), + userAgent: headers(constants.Headers.UserAgent), + }; +}; + +/** + * Load authenticate request options related to cookies. + */ +export const loadOptionsFromCookies = (cookies: ReturnType['cookies']) => { + if (!cookies) { + return {}; + } + + return { + cookieToken: cookies?.(constants.Cookies.Session), + clientUat: cookies?.(constants.Cookies.ClientUat), }; }; From 2cb958b54443e2693d5eb5e46297ba4e75b52236 Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Mon, 13 Nov 2023 15:43:55 +0200 Subject: [PATCH 4/5] chore(backend): Drop usage of x-forwarded-port --- packages/backend/src/tokens/request.test.ts | 3 --- packages/backend/src/tokens/request.ts | 1 - 2 files changed, 4 deletions(-) diff --git a/packages/backend/src/tokens/request.test.ts b/packages/backend/src/tokens/request.test.ts index 21c5255883e..f2b24dc5ff2 100644 --- a/packages/backend/src/tokens/request.test.ts +++ b/packages/backend/src/tokens/request.test.ts @@ -622,7 +622,6 @@ export default (QUnit: QUnit) => { origin: '', host: '', forwardedHost: '', - forwardedPort: '', forwardedProto: '', referrer: '', userAgent: '', @@ -635,7 +634,6 @@ export default (QUnit: QUnit) => { assert.propEqual(loadOptionsFromHeaders(headers), { ...defaultOptions, forwardedProto: 'http', - forwardedPort: '80', forwardedHost: 'example.com', }); }); @@ -652,7 +650,6 @@ export default (QUnit: QUnit) => { assert.propEqual(loadOptionsFromHeaders(headers), { ...defaultOptions, forwardedProto: 'https', - forwardedPort: '80', forwardedHost: 'example.com', }); }); diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index 5f37e9e9562..dff88a8d33c 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -152,7 +152,6 @@ export const loadOptionsFromHeaders = (headers: ReturnType[ origin: headers(constants.Headers.Origin), host: headers(constants.Headers.Host), forwardedHost: headers(constants.Headers.ForwardedHost), - forwardedPort: headers(constants.Headers.ForwardedPort), forwardedProto: headers(constants.Headers.CloudFrontForwardedProto) || headers(constants.Headers.ForwardedProto), referrer: headers(constants.Headers.Referrer), userAgent: headers(constants.Headers.UserAgent), From eaa8e1bbeff29a10fe8137bccf557093562bc493 Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Mon, 13 Nov 2023 18:04:48 +0200 Subject: [PATCH 5/5] chore(repo): Add changeset --- .changeset/red-coats-itch.md | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .changeset/red-coats-itch.md diff --git a/.changeset/red-coats-itch.md b/.changeset/red-coats-itch.md new file mode 100644 index 00000000000..710c6130ec8 --- /dev/null +++ b/.changeset/red-coats-itch.md @@ -0,0 +1,57 @@ +--- +'@clerk/backend': major +--- + +Enforce passing `request` param to `authenticateRequest` method of `@clerk/backend` +instead of passing each header or cookie related option that is used internally to +determine the request state. + +Migration guide: +- use `request` param in `clerkClient.authenticateRequest()` instead of: + - `origin` + - `host` + - `forwardedHost` + - `forwardedProto` + - `referrer` + - `userAgent` + - `cookieToken` + - `clientUat` + - `headerToken` + - `searchParams` + +Example +```typescript +// +// current +// +import { clerkClient } from '@clerk/backend' + +const requestState = await clerkClient.authenticateRequest({ + secretKey: 'sk_....' + publishableKey: 'pk_....' + origin: req.headers.get('origin'), + host: req.headers.get('host'), + forwardedHost: req.headers.get('x-forwarded-host'), + forwardedProto: req.headers.get('x-forwarded-proto'), + referrer: req.headers.get('referer'), + userAgent: req.headers.get('user-agent'), + clientUat: req.cookies.get('__client_uat'), + cookieToken: req.cookies.get('__session'), + headerToken: req.headers.get('authorization'), + searchParams: req.searchParams +}); + +// +// new +// +import { clerkClient, } from '@clerk/backend' + +// use req (if it's a fetch#Request instance) or use `createIsomorphicRequest` from `@clerk/backend` +// to re-construct fetch#Request instance +const requestState = await clerkClient.authenticateRequest({ + secretKey: 'sk_....' + publishableKey: 'pk_....' + request: req +}); + +``` \ No newline at end of file