Skip to content

Commit 88f877a

Browse files
brkalowSokratisVidros
authored andcommitted
feat(backend): Update authenticateRequest handler to support multi-domain handshake
1 parent 23046d5 commit 88f877a

File tree

7 files changed

+103
-90
lines changed

7 files changed

+103
-90
lines changed

packages/backend/src/tokens/authStatus.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export type SignedInState = {
2929
isInterstitial: false;
3030
isUnknown: false;
3131
toAuth: () => SignedInAuthObject;
32-
headers: Headers | null;
32+
headers: Headers;
3333
};
3434

3535
export type SignedOutState = {
@@ -48,7 +48,7 @@ export type SignedOutState = {
4848
isInterstitial: false;
4949
isUnknown: false;
5050
toAuth: () => SignedOutAuthObject;
51-
headers: Headers | null;
51+
headers: Headers;
5252
};
5353

5454
export type InterstitialState = Omit<SignedOutState, 'isInterstitial' | 'status' | 'toAuth'> & {
@@ -136,7 +136,7 @@ export type AuthStatusOptionsType = LoadResourcesOptions &
136136
export async function signedIn<T extends AuthStatusOptionsType>(
137137
options: T,
138138
sessionClaims: JwtPayload,
139-
headers: Headers | null = null,
139+
headers: Headers = new Headers(),
140140
): Promise<SignedInState> {
141141
const {
142142
publishableKey = '',
@@ -212,7 +212,7 @@ export function signedOut<T extends AuthStatusOptionsType>(
212212
options: T,
213213
reason: AuthReason,
214214
message = '',
215-
headers: Headers | null = null,
215+
headers: Headers = new Headers(),
216216
): SignedOutState {
217217
const {
218218
publishableKey = '',

packages/backend/src/tokens/request.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { parsePublishableKey } from '@clerk/shared/keys';
33
import { constants } from '../constants';
44
import { assertValidSecretKey } from '../util/assertValidSecretKey';
55
import { buildRequest, stripAuthorizationHeader } from '../util/IsomorphicRequest';
6-
import { isDevelopmentFromSecretKey } from '../util/shared';
6+
import { addClerkPrefix, isDevelopmentFromSecretKey, isDevOrStagingUrl } from '../util/shared';
7+
import { buildRequestUrl } from '../utils';
78
import type { AuthStatusOptionsType, RequestState } from './authStatus';
89
import { AuthErrorReason, handshake, interstitial, signedIn, signedOut, unknownState } from './authStatus';
910
import type { TokenCarrier } from './errors';
@@ -81,14 +82,21 @@ export async function authenticateRequest(options: AuthenticateRequestOptions):
8182
publishableKey,
8283
devBrowserToken,
8384
redirectUrl,
85+
domain,
86+
proxyUrl,
8487
}: {
8588
publishableKey: string;
8689
devBrowserToken: string;
8790
redirectUrl: string;
91+
domain?: string;
92+
proxyUrl?: string;
8893
}): string {
8994
const pk = parsePublishableKey(publishableKey);
95+
const pkFapi = pk?.frontendApi || '';
96+
// determine proper FAPI url, taking into account multi-domain setups
97+
const frontendApi = proxyUrl || (!isDevOrStagingUrl(pkFapi) ? addClerkPrefix(domain) : '') || pkFapi;
9098

91-
const url = new URL(`https://${pk?.frontendApi}/v1/client/handshake`);
99+
const url = new URL(`https://${frontendApi}/v1/client/handshake`);
92100
url.searchParams.append('redirect_url', redirectUrl);
93101

94102
if (pk?.instanceType === 'development' && devBrowserToken) {
@@ -135,6 +143,14 @@ export async function authenticateRequest(options: AuthenticateRequestOptions):
135143
}
136144
});
137145

146+
if (parsePublishableKey(options.publishableKey)?.instanceType === 'development') {
147+
const newUrl = new URL(options.request.url);
148+
newUrl.searchParams.delete('__clerk_handshake');
149+
newUrl.searchParams.delete('__clerk_help');
150+
151+
headers.append('Location', newUrl.toString());
152+
}
153+
138154
if (sessionToken === '') {
139155
return signedOut(ruleOptions, AuthErrorReason.SessionTokenMissing, '', headers);
140156
}
@@ -150,6 +166,24 @@ export async function authenticateRequest(options: AuthenticateRequestOptions):
150166
}
151167

152168
// ================ This is to start the handshake if necessary ===================
169+
if (ruleOptions.isSatellite && !new URL(options.request.url).searchParams.has('__clerk_synced')) {
170+
const redirectUrl = buildRequestUrl(options.request);
171+
const headers = new Headers();
172+
headers.set(
173+
'Location',
174+
buildRedirectToHandshake({
175+
publishableKey: ruleOptions.publishableKey!,
176+
devBrowserToken: ruleOptions.devBrowserToken!,
177+
redirectUrl: redirectUrl.toString(),
178+
proxyUrl: ruleOptions.proxyUrl,
179+
domain: ruleOptions.domain,
180+
}),
181+
);
182+
183+
// TODO: Add status code for redirection
184+
return handshake(ruleOptions, AuthErrorReason.SatelliteCookieNeedsSyncing, '', headers);
185+
}
186+
153187
if (!hasActiveClient && !hasSessionToken) {
154188
return signedOut(ruleOptions, AuthErrorReason.CookieAndUATMissing);
155189
}

packages/clerk-js/src/core/devBrowserHandler.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export interface DevBrowserHandler {
1414

1515
setup(): Promise<void>;
1616

17-
getDevBrowserJWT(): string | null;
17+
getDevBrowserJWT(): string | undefined;
1818

1919
setDevBrowserJWT(jwt: string): void;
2020

@@ -39,11 +39,10 @@ export function createDevBrowserHandler({
3939
let usesUrlBasedSessionSyncing = true;
4040

4141
function getDevBrowserJWT() {
42-
return localStorage.getItem(key);
42+
return cookieHandler.getDevBrowserCookie();
4343
}
4444

4545
function setDevBrowserJWT(jwt: string) {
46-
localStorage.setItem(key, jwt);
4746
// Append dev browser JWT to cookies, because server-side redirects (e.g. middleware) has no access to local storage
4847
cookieHandler.setDevBrowserCookie(jwt);
4948
}
@@ -116,7 +115,7 @@ export function createDevBrowserHandler({
116115
}
117116

118117
// 2. If no JWT is found in the first step, check if a JWT is already available in the local cache
119-
if (getDevBrowserJWT() !== null) {
118+
if (getDevBrowserJWT()) {
120119
return;
121120
}
122121

packages/nextjs/src/server/authMiddleware.ts

Lines changed: 53 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { AuthObject, RequestState } from '@clerk/backend';
2-
import { buildRequestUrl, constants, TokenVerificationErrorReason } from '@clerk/backend';
1+
import type { AuthenticateRequestOptions, AuthObject } from '@clerk/backend';
2+
import { AuthStatus, buildRequestUrl, constants } from '@clerk/backend';
33
import { DEV_BROWSER_JWT_MARKER, setDevBrowserJWTInURL } from '@clerk/shared/devBrowser';
44
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
55
import { eventMethodCalled } from '@clerk/shared/telemetry';
@@ -10,15 +10,10 @@ import { NextResponse } from 'next/server';
1010

1111
import { isRedirect, mergeResponses, paths, setHeader, stringifyHeaders } from '../utils';
1212
import { withLogger } from '../utils/debugLogger';
13-
import { authenticateRequest, handleInterstitialState, handleUnknownState } from './authenticateRequest';
13+
import { authenticateRequest } from './authenticateRequest';
1414
import { clerkClient } from './clerkClient';
1515
import { SECRET_KEY } from './constants';
16-
import {
17-
clockSkewDetected,
18-
infiniteRedirectLoopDetected,
19-
informAboutProtectedRouteInfo,
20-
receivedRequestForIgnoredRoute,
21-
} from './errors';
16+
import { informAboutProtectedRouteInfo, receivedRequestForIgnoredRoute } from './errors';
2217
import { redirectToSignIn } from './redirect';
2318
import type { NextMiddlewareResult, WithAuthOptions } from './types';
2419
import { isDevAccountPortalOrigin } from './url';
@@ -39,8 +34,6 @@ type RouteMatcherWithNextTypedRoutes = Autocomplete<
3934
WithPathPatternWildcard<ExcludeRootPath<NextTypedRoute>> | NextTypedRoute
4035
>;
4136

42-
const INFINITE_REDIRECTION_LOOP_COOKIE = '__clerk_redirection_loop';
43-
4437
/**
4538
* The default ideal matcher that excludes the _next directory (internals) and all static files,
4639
* but it will match the root route (/) and any routes that start with /api or /trpc.
@@ -191,22 +184,50 @@ const authMiddleware: AuthMiddleware = (...args: unknown[]) => {
191184
return setHeader(beforeAuthRes, constants.Headers.AuthReason, 'redirect');
192185
}
193186

194-
const requestState = await authenticateRequest(req, options);
195-
if (requestState.isUnknown) {
196-
logger.debug('authenticateRequest state is unknown', requestState);
197-
return handleUnknownState(requestState);
198-
} else if (requestState.isInterstitial && isApiRoute(req)) {
199-
logger.debug('authenticateRequest state is interstitial in an API route', requestState);
200-
return handleUnknownState(requestState);
201-
} else if (requestState.isInterstitial) {
202-
logger.debug('authenticateRequest state is interstitial', requestState);
187+
const devBrowserToken =
188+
req.nextUrl.searchParams.get('__clerk_db_jwt') || req.cookies.get('__clerk_db_jwt')?.value || '';
189+
const handshakeToken =
190+
req.nextUrl.searchParams.get('__clerk_handshake') || req.cookies.get('__clerk_handshake')?.value || '';
191+
192+
// TODO: fix type discrepancy between WithAuthOptions and AuthenticateRequestOptions
193+
const requestState = await authenticateRequest(req, {
194+
...options,
195+
devBrowserToken,
196+
handshakeToken,
197+
} as AuthenticateRequestOptions);
198+
const requestStateHeaders = requestState.headers;
199+
200+
const locationHeader = requestStateHeaders?.get('location');
203201

204-
assertClockSkew(requestState, options);
202+
// triggering a handshake redirect
203+
if (locationHeader) {
204+
return new Response(null, { status: 307, headers: requestStateHeaders });
205+
}
205206

206-
const res = handleInterstitialState(requestState, options);
207-
return assertInfiniteRedirectionLoop(req, res, options, requestState);
207+
if (
208+
requestState.status === AuthStatus.Handshake ||
209+
requestState.status === AuthStatus.Unknown ||
210+
requestState.status === AuthStatus.Interstitial
211+
) {
212+
console.log(requestState);
213+
throw new Error('Unexpected handshake or unknown state without redirect');
208214
}
209215

216+
// if (requestState.isUnknown) {
217+
// logger.debug('authenticateRequest state is unknown', requestState);
218+
// return handleUnknownState(requestState);
219+
// } else if (requestState.isInterstitial && isApiRoute(req)) {
220+
// logger.debug('authenticateRequest state is interstitial in an API route', requestState);
221+
// return handleUnknownState(requestState);
222+
// } else if (requestState.isInterstitial) {
223+
// logger.debug('authenticateRequest state is interstitial', requestState);
224+
225+
// assertClockSkew(requestState, options);
226+
227+
// const res = handleInterstitialState(requestState, options);
228+
// return assertInfiniteRedirectionLoop(req, res, options, requestState);
229+
// }
230+
210231
const auth = Object.assign(requestState.toAuth(), {
211232
isPublicRoute: isPublicRoute(req),
212233
isApiRoute: isApiRoute(req),
@@ -227,7 +248,15 @@ const authMiddleware: AuthMiddleware = (...args: unknown[]) => {
227248
logger.debug(`Added ${constants.Headers.EnableDebug} on request`);
228249
}
229250

230-
return decorateRequest(req, finalRes, requestState);
251+
const result = decorateRequest(req, finalRes, requestState) || NextResponse.next();
252+
253+
if (requestStateHeaders) {
254+
requestStateHeaders.forEach((value, key) => {
255+
result.headers.append(key, value);
256+
});
257+
}
258+
259+
return result;
231260
});
232261
};
233262

@@ -352,55 +381,6 @@ const isRequestMethodIndicatingApiRoute = (req: NextRequest): boolean => {
352381
return !['get', 'head', 'options'].includes(requestMethod);
353382
};
354383

355-
/**
356-
* In development, attempt to detect clock skew based on the requestState. This check should run when requestState.isInterstitial is true. If detected, we throw an error.
357-
*/
358-
const assertClockSkew = (requestState: RequestState, opts: AuthMiddlewareParams): void => {
359-
if (!isDevelopmentFromSecretKey(opts.secretKey || SECRET_KEY)) {
360-
return;
361-
}
362-
363-
if (requestState.reason === TokenVerificationErrorReason.TokenNotActiveYet) {
364-
throw new Error(clockSkewDetected(requestState.message));
365-
}
366-
};
367-
368-
// When in development, we want to prevent infinite interstitial redirection loops.
369-
// We incrementally set a `__clerk_redirection_loop` cookie, and when it loops 6 times, we throw an error.
370-
// We also utilize the `referer` header to skip the prefetch requests.
371-
const assertInfiniteRedirectionLoop = (
372-
req: NextRequest,
373-
res: NextResponse,
374-
opts: AuthMiddlewareParams,
375-
requestState: RequestState,
376-
): NextResponse => {
377-
if (!isDevelopmentFromSecretKey(opts.secretKey || SECRET_KEY)) {
378-
return res;
379-
}
380-
381-
const infiniteRedirectsCounter = Number(req.cookies.get(INFINITE_REDIRECTION_LOOP_COOKIE)?.value) || 0;
382-
if (infiniteRedirectsCounter === 6) {
383-
// Infinite redirect detected, is it clock skew?
384-
// We check for token-expired here because it can be a valid, recoverable scenario, but in a redirect loop a token-expired error likely indicates clock skew.
385-
if (requestState.reason === TokenVerificationErrorReason.TokenExpired) {
386-
throw new Error(clockSkewDetected(requestState.message));
387-
}
388-
389-
// Not clock skew, return general error
390-
throw new Error(infiniteRedirectLoopDetected());
391-
}
392-
393-
// Skip the prefetch requests (when hovering a Next Link element)
394-
if (req.headers.get('referer') === req.url) {
395-
res.cookies.set({
396-
name: INFINITE_REDIRECTION_LOOP_COOKIE,
397-
value: `${infiniteRedirectsCounter + 1}`,
398-
maxAge: 3,
399-
});
400-
}
401-
return res;
402-
};
403-
404384
const withNormalizedClerkUrl = (req: NextRequest): WithClerkUrl<NextRequest> => {
405385
const clerkUrl = req.nextUrl.clone();
406386

packages/nextjs/src/server/authenticateRequest.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1+
import type { AuthenticateRequestOptions } from '@clerk/backend';
12
import { constants, debugRequestState } from '@clerk/backend';
23
import type { NextRequest } from 'next/server';
34
import { NextResponse } from 'next/server';
45

56
import type { RequestState } from './clerkClient';
67
import { clerkClient } from './clerkClient';
78
import { CLERK_JS_URL, CLERK_JS_VERSION, PUBLISHABLE_KEY, SECRET_KEY } from './constants';
8-
import type { WithAuthOptions } from './types';
99
import { apiEndpointUnauthorizedNextResponse, handleMultiDomainAndProxy } from './utils';
1010

11-
export const authenticateRequest = async (req: NextRequest, opts: WithAuthOptions) => {
11+
export const authenticateRequest = async (req: NextRequest, opts: AuthenticateRequestOptions) => {
1212
const { isSatellite, domain, signInUrl, proxyUrl } = handleMultiDomainAndProxy(req, opts);
1313
return await clerkClient.authenticateRequest({
1414
...opts,
@@ -34,7 +34,7 @@ export const handleUnknownState = (requestState: RequestState) => {
3434
return response;
3535
};
3636

37-
export const handleInterstitialState = (requestState: RequestState, opts: WithAuthOptions) => {
37+
export const handleInterstitialState = (requestState: RequestState, opts: AuthenticateRequestOptions) => {
3838
const response = new NextResponse(
3939
clerkClient.localInterstitial({
4040
publishableKey: opts.publishableKey || PUBLISHABLE_KEY,

packages/nextjs/src/server/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { RequestState } from '@clerk/backend';
1+
import type { AuthenticateRequestOptions, RequestState } from '@clerk/backend';
22
import { buildRequestUrl, constants } from '@clerk/backend';
33
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
44
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
@@ -9,7 +9,7 @@ import { NextResponse } from 'next/server';
99
import { constants as nextConstants } from '../constants';
1010
import { DOMAIN, IS_SATELLITE, PROXY_URL, SECRET_KEY, SIGN_IN_URL } from './constants';
1111
import { missingDomainAndProxy, missingSignInUrlInDev } from './errors';
12-
import type { NextMiddlewareResult, RequestLike, WithAuthOptions } from './types';
12+
import type { NextMiddlewareResult, RequestLike } from './types';
1313

1414
type AuthKey = 'AuthStatus' | 'AuthMessage' | 'AuthReason';
1515

@@ -221,7 +221,7 @@ export const isCrossOrigin = (from: string | URL, to: string | URL) => {
221221
return fromUrl.origin !== toUrl.origin;
222222
};
223223

224-
export const handleMultiDomainAndProxy = (req: NextRequest, opts: WithAuthOptions) => {
224+
export const handleMultiDomainAndProxy = (req: NextRequest, opts: AuthenticateRequestOptions) => {
225225
const requestURL = buildRequestUrl(req);
226226
const relativeOrAbsoluteProxyUrl = handleValueOrFn(opts?.proxyUrl, requestURL, PROXY_URL);
227227
let proxyUrl;

packages/shared/src/devBrowser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const DEV_BROWSER_SSO_JWT_PARAMETER = '__dev_session';
1+
export const DEV_BROWSER_SSO_JWT_PARAMETER = '__clerk_db_jwt';
22
export const DEV_BROWSER_JWT_MARKER = '__clerk_db_jwt';
33
export const DEV_BROWSER_SSO_JWT_KEY = 'clerk-db-jwt';
44

0 commit comments

Comments
 (0)