Skip to content

Commit e1773ef

Browse files
authored
fix(react-router): Forward redirect URL options from middleware to client state (#8622)
1 parent a47d66e commit e1773ef

7 files changed

Lines changed: 161 additions & 30 deletions

File tree

.changeset/plain-apes-sneeze.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clerk/react-router": patch
3+
---
4+
5+
Forward redirect URL options from middleware to client state.

packages/react-router/src/server/__tests__/clerkMiddleware.test.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,10 @@ describe('clerkMiddleware', () => {
7979
});
8080

8181
expect(mockContext.set).toHaveBeenCalledWith(authFnContext, expect.any(Function));
82-
expect(mockContext.set).toHaveBeenCalledWith(requestStateContext, mockRequestState);
82+
expect(mockContext.set).toHaveBeenCalledWith(
83+
requestStateContext,
84+
expect.objectContaining({ requestState: mockRequestState }),
85+
);
8386

8487
expect(mockNext).toHaveBeenCalled();
8588

@@ -119,6 +122,54 @@ describe('clerkMiddleware', () => {
119122
expect(mockLoadOptions).toHaveBeenCalledWith(args, options);
120123
});
121124

125+
it('should set redirect URL options from loadOptions in additionalStateContext', async () => {
126+
mockLoadOptions.mockReturnValue({
127+
audience: '',
128+
authorizedParties: [],
129+
signInUrl: '',
130+
signUpUrl: '',
131+
secretKey: 'sk_test_...',
132+
publishableKey: 'pk_test_...',
133+
signInForceRedirectUrl: '/dashboard',
134+
signUpForceRedirectUrl: '/welcome',
135+
signInFallbackRedirectUrl: '/home',
136+
signUpFallbackRedirectUrl: '/home',
137+
} as unknown as ReturnType<typeof loadOptions>);
138+
139+
const mockRequestState = {
140+
status: AuthStatus.SignedIn,
141+
headers: new Headers(),
142+
toAuth: vi.fn().mockReturnValue({ userId: 'user_xxx', tokenType: TokenType.SessionToken }),
143+
};
144+
145+
mockClerkClient.mockReturnValue({
146+
authenticateRequest: vi.fn().mockResolvedValue(mockRequestState),
147+
} as unknown as ClerkClient);
148+
149+
const middleware = clerkMiddleware();
150+
const args = {
151+
request: new Request('http://clerk.com'),
152+
context: mockContext,
153+
} as LoaderFunctionArgs;
154+
155+
mockNext.mockResolvedValue(new Response('OK'));
156+
157+
await middleware(args, mockNext);
158+
159+
expect(mockContext.set).toHaveBeenCalledWith(
160+
requestStateContext,
161+
expect.objectContaining({
162+
requestState: mockRequestState,
163+
additionalState: expect.objectContaining({
164+
signInForceRedirectUrl: '/dashboard',
165+
signUpForceRedirectUrl: '/welcome',
166+
signInFallbackRedirectUrl: '/home',
167+
signUpFallbackRedirectUrl: '/home',
168+
}),
169+
}),
170+
);
171+
});
172+
122173
it('should append request state headers to response', async () => {
123174
const mockRequestState = {
124175
status: AuthStatus.SignedIn,

packages/react-router/src/server/__tests__/rootAuthLoader.test.ts

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,21 @@ describe('rootAuthLoader', () => {
1212
});
1313

1414
describe('with middleware context', () => {
15+
const mockRequestState = {
16+
toAuth: vi.fn().mockImplementation(() => ({
17+
userId: 'user_xxx',
18+
tokenType: TokenType.SessionToken,
19+
})),
20+
headers: new Headers(),
21+
status: 'signed-in',
22+
};
23+
1524
const mockContext = {
1625
get: vi.fn().mockImplementation(contextKey => {
1726
if (contextKey === requestStateContext) {
1827
return {
19-
toAuth: vi.fn().mockImplementation(() => ({
20-
userId: 'user_xxx',
21-
tokenType: TokenType.SessionToken,
22-
})),
23-
headers: new Headers(),
24-
status: 'signed-in',
28+
requestState: mockRequestState,
29+
additionalState: {},
2530
};
2631
}
2732
if (contextKey === authFnContext) {
@@ -101,5 +106,39 @@ describe('rootAuthLoader', () => {
101106

102107
expect(result).toHaveProperty('clerkState');
103108
});
109+
110+
it('should forward redirect URL options from additionalState into clerkState', async () => {
111+
const mockContext2 = {
112+
get: vi.fn().mockImplementation(contextKey => {
113+
if (contextKey === requestStateContext) {
114+
return {
115+
requestState: mockRequestState,
116+
additionalState: {
117+
signInForceRedirectUrl: '/dashboard',
118+
signUpForceRedirectUrl: '/welcome',
119+
signInFallbackRedirectUrl: '/home',
120+
signUpFallbackRedirectUrl: '/home',
121+
},
122+
};
123+
}
124+
if (contextKey === authFnContext) {
125+
return vi.fn().mockReturnValue({ userId: 'user_xxx', tokenType: TokenType.SessionToken });
126+
}
127+
return null;
128+
}),
129+
set: vi.fn(),
130+
};
131+
132+
const result = (await rootAuthLoader({
133+
context: mockContext2,
134+
request: new Request('http://clerk.com'),
135+
} as LoaderFunctionArgs)) as any;
136+
137+
const internalState = result.clerkState.__internal_clerk_state;
138+
expect(internalState.__signInForceRedirectUrl).toBe('/dashboard');
139+
expect(internalState.__signUpForceRedirectUrl).toBe('/welcome');
140+
expect(internalState.__signInFallbackRedirectUrl).toBe('/home');
141+
expect(internalState.__signUpFallbackRedirectUrl).toBe('/home');
142+
});
104143
});
105144
});

packages/react-router/src/server/clerkMiddleware.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@ import { createContext } from 'react-router';
99
import { clerkClient } from './clerkClient';
1010
import { resolveKeysWithKeylessFallback } from './keyless/utils';
1111
import { loadOptions } from './loadOptions';
12-
import type { ClerkMiddlewareOptions } from './types';
12+
import type { AdditionalStateOptions, ClerkMiddlewareOptions } from './types';
1313
import { patchRequest } from './utils';
1414

15+
type RequestStateContextValue = {
16+
requestState: RequestState<any>;
17+
additionalState: AdditionalStateOptions;
18+
};
19+
1520
export const authFnContext = createContext<((options?: PendingSessionOptions) => AuthObject) | null>(null);
16-
export const requestStateContext = createContext<RequestState<any> | null>(null);
21+
export const requestStateContext = createContext<RequestStateContextValue | null>(null);
1722

1823
/**
1924
* Middleware that integrates Clerk authentication into your React Router application.
@@ -83,11 +88,6 @@ export const clerkMiddleware = (options?: ClerkMiddlewareOptions): MiddlewareFun
8388
acceptsToken: 'any',
8489
});
8590

86-
Object.assign(requestState, {
87-
__keylessClaimUrl,
88-
__keylessApiKeysUrl,
89-
});
90-
9191
const locationHeader = requestState.headers.get(constants.Headers.Location);
9292
if (locationHeader) {
9393
handleNetlifyCacheInDevInstance({
@@ -104,7 +104,17 @@ export const clerkMiddleware = (options?: ClerkMiddlewareOptions): MiddlewareFun
104104
}
105105

106106
args.context.set(authFnContext, (opts?: PendingSessionOptions) => requestState.toAuth(opts));
107-
args.context.set(requestStateContext, requestState);
107+
args.context.set(requestStateContext, {
108+
requestState,
109+
additionalState: {
110+
__keylessClaimUrl,
111+
__keylessApiKeysUrl,
112+
signInForceRedirectUrl: loadedOptions.signInForceRedirectUrl,
113+
signUpForceRedirectUrl: loadedOptions.signUpForceRedirectUrl,
114+
signInFallbackRedirectUrl: loadedOptions.signInFallbackRedirectUrl,
115+
signUpFallbackRedirectUrl: loadedOptions.signUpFallbackRedirectUrl,
116+
},
117+
});
108118

109119
const response = await next();
110120

packages/react-router/src/server/rootAuthLoader.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { LoaderFunctionArgs } from 'react-router';
44
import { invalidRootLoaderCallbackReturn } from '../utils/errors';
55
import { authFnContext, requestStateContext } from './clerkMiddleware';
66
import type {
7+
AdditionalStateOptions,
78
LoaderFunctionArgsWithAuth,
89
LoaderFunctionReturn,
910
RootAuthLoaderCallback,
@@ -40,14 +41,15 @@ interface RootAuthLoader {
4041
async function processRootAuthLoader(
4142
args: LoaderFunctionArgs,
4243
requestState: RequestState,
44+
additionalState: AdditionalStateOptions,
4345
handler?: RootAuthLoaderCallback<any>,
4446
): Promise<LoaderFunctionReturn> {
4547
const hasMiddleware = IsOptIntoMiddleware(args.context) && !!args.context.get(authFnContext);
4648
const includeClerkHeaders = !hasMiddleware;
4749

4850
if (!handler) {
4951
// if the user did not provide a handler, simply inject requestState into an empty response
50-
const { clerkState } = getResponseClerkState(requestState, args.context);
52+
const { clerkState } = getResponseClerkState(requestState, args.context, additionalState);
5153
return {
5254
...clerkState,
5355
};
@@ -69,7 +71,13 @@ async function processRootAuthLoader(
6971
}
7072
// clone and try to inject requestState into all json-like responses
7173
// if this fails, the user probably didn't return a json object or a valid json string
72-
return injectRequestStateIntoResponse(handlerResult, requestState, args.context, includeClerkHeaders);
74+
return injectRequestStateIntoResponse(
75+
handlerResult,
76+
requestState,
77+
args.context,
78+
additionalState,
79+
includeClerkHeaders,
80+
);
7381
} catch {
7482
throw new Error(invalidRootLoaderCallbackReturn);
7583
}
@@ -83,6 +91,7 @@ async function processRootAuthLoader(
8391
new Response(JSON.stringify(handlerResult.data), handlerResult.init ?? undefined),
8492
requestState,
8593
args.context,
94+
additionalState,
8695
includeClerkHeaders,
8796
);
8897
} catch {
@@ -91,7 +100,7 @@ async function processRootAuthLoader(
91100
}
92101

93102
// If the return value of the user's handler is null or a plain object, return plain object with streaming support
94-
const { clerkState } = getResponseClerkState(requestState, args.context);
103+
const { clerkState } = getResponseClerkState(requestState, args.context, additionalState);
95104

96105
return {
97106
...(handlerResult ?? {}),
@@ -111,13 +120,14 @@ export const rootAuthLoader: RootAuthLoader = async (
111120
const handler = typeof handlerOrOptions === 'function' ? handlerOrOptions : undefined;
112121

113122
const hasMiddlewareFlag = IsOptIntoMiddleware(args.context);
114-
const requestState = hasMiddlewareFlag && args.context.get(requestStateContext);
123+
const contextValue = hasMiddlewareFlag && args.context.get(requestStateContext);
115124

116-
if (!requestState) {
125+
if (!contextValue) {
117126
throw new Error(
118127
'Clerk: clerkMiddleware() not detected. Make sure you have installed the clerkMiddleware in your root route.',
119128
);
120129
}
121130

122-
return processRootAuthLoader(args, requestState, handler);
131+
const { requestState, additionalState } = contextValue;
132+
return processRootAuthLoader(args, requestState, additionalState, handler);
123133
};

packages/react-router/src/server/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ export interface KeylessUrls {
6868
__keylessApiKeysUrl?: string;
6969
}
7070

71+
export type AdditionalStateOptions = SignInFallbackRedirectUrl &
72+
SignUpFallbackRedirectUrl &
73+
SignInForceRedirectUrl &
74+
SignUpForceRedirectUrl &
75+
KeylessUrls;
76+
77+
/**
78+
* @deprecated This type is no longer used internally. Use `AdditionalStateOptions` instead.
79+
*/
7180
export type RequestStateWithRedirectUrls = RequestState &
7281
SignInForceRedirectUrl &
7382
SignInFallbackRedirectUrl &

packages/react-router/src/server/utils.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import type { RequestState } from '@clerk/backend/internal';
12
import { constants, debugRequestState } from '@clerk/backend/internal';
23
import { parse as parseCookie } from 'cookie';
34
import type { AppLoadContext, UNSAFE_DataWithResponseInit } from 'react-router';
45

56
import { getPublicEnvVariables } from '../utils/env';
67
import { canUseKeyless } from '../utils/feature-flags';
7-
import type { RequestStateWithRedirectUrls } from './types';
8+
import type { AdditionalStateOptions } from './types';
89

910
export function isResponse(value: any): value is Response {
1011
return (
@@ -51,14 +52,15 @@ export const IsOptIntoMiddleware = (context: AppLoadContext) => {
5152

5253
export const injectRequestStateIntoResponse = async (
5354
response: Response,
54-
requestState: RequestStateWithRedirectUrls,
55+
requestState: RequestState,
5556
context: AppLoadContext,
57+
additionalStateOptions: AdditionalStateOptions = {},
5658
includeClerkHeaders = false,
5759
) => {
5860
const clone = new Response(response.body, response);
5961
const data = await clone.json();
6062

61-
const { clerkState, headers } = getResponseClerkState(requestState, context);
63+
const { clerkState, headers } = getResponseClerkState(requestState, context, additionalStateOptions);
6264

6365
// set the correct content-type header in case the user returned a `Response` directly
6466
clone.headers.set(constants.Headers.ContentType, constants.ContentTypes.Json);
@@ -78,9 +80,14 @@ export const injectRequestStateIntoResponse = async (
7880
*
7981
* @internal
8082
*/
81-
export function getResponseClerkState(requestState: RequestStateWithRedirectUrls, context: AppLoadContext) {
82-
const { reason, message, isSignedIn, __keylessClaimUrl, __keylessApiKeysUrl, ...rest } = requestState;
83+
export function getResponseClerkState(
84+
requestState: RequestState,
85+
context: AppLoadContext,
86+
additionalStateOptions: AdditionalStateOptions = {},
87+
) {
88+
const { reason, message, isSignedIn, ...rest } = requestState;
8389
const envVars = getPublicEnvVariables(context);
90+
const { __keylessClaimUrl, __keylessApiKeysUrl, ...redirectUrlOptions } = additionalStateOptions;
8491

8592
const baseState: Record<string, unknown> = {
8693
__clerk_ssr_state: rest.toAuth(),
@@ -90,10 +97,10 @@ export function getResponseClerkState(requestState: RequestStateWithRedirectUrls
9097
__isSatellite: requestState.isSatellite,
9198
__signInUrl: requestState.signInUrl,
9299
__signUpUrl: requestState.signUpUrl,
93-
__signInForceRedirectUrl: requestState.signInForceRedirectUrl,
94-
__signUpForceRedirectUrl: requestState.signUpForceRedirectUrl,
95-
__signInFallbackRedirectUrl: requestState.signInFallbackRedirectUrl,
96-
__signUpFallbackRedirectUrl: requestState.signUpFallbackRedirectUrl,
100+
__signInForceRedirectUrl: redirectUrlOptions.signInForceRedirectUrl,
101+
__signUpForceRedirectUrl: redirectUrlOptions.signUpForceRedirectUrl,
102+
__signInFallbackRedirectUrl: redirectUrlOptions.signInFallbackRedirectUrl,
103+
__signUpFallbackRedirectUrl: redirectUrlOptions.signUpFallbackRedirectUrl,
97104
__clerk_debug: debugRequestState(requestState),
98105
__clerkJSUrl: envVars.clerkJsUrl,
99106
__clerkJSVersion: envVars.clerkJsVersion,

0 commit comments

Comments
 (0)