Skip to content

Commit 45844a3

Browse files
authored
feat: add redirectUri option to middleware (#29)
* implement * docs: document redirectUri middleware option * fix: align redirectUri injection pattern across URL functions Make getAuthorizationUrl use the same conditional pattern as getSignInUrl and getSignUpUrl for injecting context redirectUri.
1 parent 7c1ec14 commit 45844a3

7 files changed

Lines changed: 130 additions & 5 deletions

File tree

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,11 +510,25 @@ function ClaimsDisplay() {
510510

511511
### Middleware
512512

513-
#### `authkitMiddleware()`
513+
#### `authkitMiddleware(options?)`
514514

515515
Processes authentication on every request. Validates tokens, refreshes sessions, and provides auth context to server functions.
516516

517-
Already shown in setup, but can be imported separately if needed.
517+
```typescript
518+
import { authkitMiddleware } from '@workos/authkit-tanstack-react-start';
519+
520+
// Basic usage
521+
authkitMiddleware();
522+
523+
// With custom redirect URI (e.g., for Vercel preview deployments)
524+
authkitMiddleware({
525+
redirectUri: 'https://preview-123.example.com/api/auth/callback',
526+
});
527+
```
528+
529+
**Options:**
530+
531+
- `redirectUri` - Override the default redirect URI from `WORKOS_REDIRECT_URI`. Useful for dynamic environments like preview deployments.
518532

519533
## TypeScript
520534

src/server/auth-helpers.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ export function isAuthConfigured(): boolean {
1919
return getAuthKitContextOrNull() !== null;
2020
}
2121

22+
/**
23+
* Gets the redirect URI from middleware context if configured.
24+
*/
25+
export function getRedirectUriFromContext(): string | undefined {
26+
const ctx = getAuthKitContextOrNull();
27+
return ctx?.redirectUri;
28+
}
29+
2230
/**
2331
* Gets the session with refresh token from the current request.
2432
* Returns null if no valid session exists.

src/server/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { User } from '../types.js';
88
export interface AuthKitServerContext {
99
auth: () => AuthResult<User>;
1010
request: Request;
11+
redirectUri?: string;
1112
__setPendingHeader: (key: string, value: string) => void;
1213
}
1314

src/server/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export {
1313
export { handleCallbackRoute } from './server.js';
1414
export type { HandleCallbackOptions, HandleAuthSuccessData, OauthTokens } from './types.js';
1515

16-
export { authkitMiddleware } from './middleware.js';
16+
export { authkitMiddleware, type AuthKitMiddlewareOptions } from './middleware.js';
1717

1818
export { getAuthkit, type AuthService } from './authkit-loader.js';
1919

src/server/middleware.spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,4 +220,56 @@ describe('authkitMiddleware', () => {
220220
expect(result.response.headers.get('X-New')).toBe('added');
221221
});
222222
});
223+
224+
describe('redirectUri option', () => {
225+
it('passes redirectUri to context when provided', async () => {
226+
mockAuthkit.withAuth.mockResolvedValue({
227+
auth: { user: null },
228+
refreshedSessionData: null,
229+
});
230+
231+
authkitMiddleware({ redirectUri: 'https://custom.example.com/callback' });
232+
233+
const mockRequest = new Request('http://test.local');
234+
const mockResponse = new Response('OK', { status: 200 });
235+
236+
let capturedContext: any = null;
237+
const args = {
238+
request: mockRequest,
239+
next: vi.fn(async ({ context }: any) => {
240+
capturedContext = context;
241+
return { response: mockResponse };
242+
}),
243+
};
244+
245+
await middlewareServerCallback(args);
246+
247+
expect(capturedContext.redirectUri).toBe('https://custom.example.com/callback');
248+
});
249+
250+
it('passes undefined redirectUri when not provided', async () => {
251+
mockAuthkit.withAuth.mockResolvedValue({
252+
auth: { user: null },
253+
refreshedSessionData: null,
254+
});
255+
256+
authkitMiddleware();
257+
258+
const mockRequest = new Request('http://test.local');
259+
const mockResponse = new Response('OK', { status: 200 });
260+
261+
let capturedContext: any = null;
262+
const args = {
263+
request: mockRequest,
264+
next: vi.fn(async ({ context }: any) => {
265+
capturedContext = context;
266+
return { response: mockResponse };
267+
}),
268+
};
269+
270+
await middlewareServerCallback(args);
271+
272+
expect(capturedContext.redirectUri).toBeUndefined();
273+
});
274+
});
223275
});

src/server/middleware.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@ import { getAuthkit, validateConfig } from './authkit-loader.js';
33

44
let configValidated = false;
55

6+
/**
7+
* Options for AuthKit middleware.
8+
*/
9+
export interface AuthKitMiddlewareOptions {
10+
/**
11+
* Override the default redirect URI for OAuth callbacks.
12+
* Useful for dynamic environments like Vercel preview deployments.
13+
*/
14+
redirectUri?: string;
15+
}
16+
617
/**
718
* AuthKit middleware for TanStack Start.
819
* Validates/refreshes sessions and provides auth context to downstream handlers.
@@ -16,8 +27,16 @@ let configValidated = false;
1627
* requestMiddleware: [authkitMiddleware()],
1728
* }));
1829
* ```
30+
*
31+
* @example
32+
* ```typescript
33+
* // With custom redirect URI
34+
* authkitMiddleware({
35+
* redirectUri: 'https://preview.example.com/callback',
36+
* })
37+
* ```
1938
*/
20-
export const authkitMiddleware = () => {
39+
export const authkitMiddleware = (options?: AuthKitMiddlewareOptions) => {
2140
return createMiddleware().server(async (args) => {
2241
const authkit = await getAuthkit();
2342

@@ -33,6 +52,7 @@ export const authkitMiddleware = () => {
3352
context: {
3453
auth: () => auth,
3554
request: args.request,
55+
redirectUri: options?.redirectUri,
3656
__setPendingHeader: (key: string, value: string) => {
3757
// Use append for Set-Cookie to support multiple cookies
3858
if (key.toLowerCase() === 'set-cookie') {

src/server/server-functions.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { redirect } from '@tanstack/react-router';
22
import { createServerFn } from '@tanstack/react-start';
33
import type { Impersonator, User } from '../types.js';
4-
import { getRawAuthFromContext, refreshSession } from './auth-helpers.js';
4+
import { getRawAuthFromContext, refreshSession, getRedirectUriFromContext } from './auth-helpers.js';
55
import { getAuthkit } from './authkit-loader.js';
66

77
// Type-only import - safe for bundling
@@ -159,6 +159,16 @@ export const getAuthorizationUrl = createServerFn({ method: 'GET' })
159159
.inputValidator((options?: GetAuthURLOptions) => options)
160160
.handler(async ({ data: options = {} }) => {
161161
const authkit = await getAuthkit();
162+
const contextRedirectUri = getRedirectUriFromContext();
163+
164+
// Only inject context redirectUri if it exists and user didn't provide one
165+
if (contextRedirectUri && !options.redirectUri) {
166+
return authkit.getAuthorizationUrl({
167+
...options,
168+
redirectUri: contextRedirectUri,
169+
});
170+
}
171+
162172
return authkit.getAuthorizationUrl(options);
163173
});
164174

@@ -185,7 +195,17 @@ export const getSignInUrl = createServerFn({ method: 'GET' })
185195
.inputValidator((data?: string | SignInUrlOptions) => data)
186196
.handler(async ({ data }) => {
187197
const options = typeof data === 'string' ? { returnPathname: data } : data;
198+
const contextRedirectUri = getRedirectUriFromContext();
188199
const authkit = await getAuthkit();
200+
201+
// Only inject context redirectUri if it exists and user didn't provide one
202+
if (contextRedirectUri && !options?.redirectUri) {
203+
return authkit.getSignInUrl({
204+
...options,
205+
redirectUri: contextRedirectUri,
206+
});
207+
}
208+
189209
return authkit.getSignInUrl(options);
190210
});
191211

@@ -209,7 +229,17 @@ export const getSignUpUrl = createServerFn({ method: 'GET' })
209229
.inputValidator((data?: string | SignInUrlOptions) => data)
210230
.handler(async ({ data }) => {
211231
const options = typeof data === 'string' ? { returnPathname: data } : data;
232+
const contextRedirectUri = getRedirectUriFromContext();
212233
const authkit = await getAuthkit();
234+
235+
// Only inject context redirectUri if it exists and user didn't provide one
236+
if (contextRedirectUri && !options?.redirectUri) {
237+
return authkit.getSignUpUrl({
238+
...options,
239+
redirectUri: contextRedirectUri,
240+
});
241+
}
242+
213243
return authkit.getSignUpUrl(options);
214244
});
215245

0 commit comments

Comments
 (0)