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..c5200cfe64e 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({ @@ -61,9 +57,7 @@ export function createAuthenticateRequest(params: CreateAuthenticateRequestOptio publishableKey: runtimePublishableKey || buildtimePublishableKey, isSatellite: runtimeIsSatellite || buildtimeIsSatellite, domain: runtimeDomain || buildtimeDomain, - jwtKey: runtimeJwtKey || buildtimeJwtKey, - searchParams: runtimeSearchParams, - userAgent: runtimeUserAgent?.toString() || buildtimeUserAgent, + jwtKey: runtimeJwtKey || buildtimeJwtKey }); }; 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), }; };