-
Notifications
You must be signed in to change notification settings - Fork 361
fix(backend,nextjs): Allow satellite redirect to public route #6225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
25660a2
69ba85b
dc8a887
fff4b67
5f1ada9
a6245cb
76f9591
5512ce4
660e1a7
341a636
fdbe6f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
'@clerk/backend': patch | ||
'@clerk/nextjs': patch | ||
'@clerk/astro': patch | ||
--- | ||
|
||
Updates the redirect logic in satellite applications to permit navigation to public routes after sign-in or sign-up. Previously, users were always redirected to the primary domain or a protected route, even when the original destination was public. This change improves support for more flexible redirect flows in multi-domain setups. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -168,6 +168,18 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl | |
logger.debug('options', options); | ||
logger.debug('url', () => clerkRequest.toJSON()); | ||
|
||
// Initial request debugging | ||
logger.debug('🚀 Initial middleware request', () => ({ | ||
url: clerkRequest.clerkUrl.toString(), | ||
method: request.method, | ||
isSatellite: options.isSatellite, | ||
domain: options.domain, | ||
signInUrl: options.signInUrl, | ||
hasSessionCookie: !!clerkRequest.cookies.get(constants.Cookies.Session), | ||
hasClientUatCookie: !!clerkRequest.cookies.get(constants.Cookies.ClientUat), | ||
queryParams: Object.fromEntries(clerkRequest.clerkUrl.searchParams.entries()), | ||
})); | ||
|
||
const authHeader = request.headers.get(constants.Headers.Authorization); | ||
if (authHeader && authHeader.startsWith('Basic ')) { | ||
logger.debug('Basic Auth detected'); | ||
|
@@ -180,7 +192,7 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl | |
})); | ||
} | ||
|
||
const requestState = await resolvedClerkClient.authenticateRequest( | ||
let requestState = await resolvedClerkClient.authenticateRequest( | ||
clerkRequest, | ||
createAuthenticateRequestOptions(clerkRequest, options), | ||
); | ||
|
@@ -199,11 +211,113 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl | |
throw new Error('Clerk: handshake status without redirect'); | ||
} | ||
|
||
// Check if we've just completed a satellite domain handshake and should re-authenticate | ||
// This fixes the issue where auth state appears signed out after satellite handshake completion | ||
const debugInfo = { | ||
isSatellite: requestState.isSatellite, | ||
currentStatus: requestState.status, | ||
url: clerkRequest.clerkUrl.toString(), | ||
hasClerkSynced: clerkRequest.clerkUrl.searchParams.has(constants.QueryParameters.ClerkSynced), | ||
hasDevBrowser: clerkRequest.clerkUrl.searchParams.has(constants.QueryParameters.DevBrowser), | ||
hasSessionCookie: !!clerkRequest.cookies.get(constants.Cookies.Session), | ||
hasClientUatCookie: !!clerkRequest.cookies.get(constants.Cookies.ClientUat), | ||
sessionCookieValue: clerkRequest.cookies.get(constants.Cookies.Session) ? '[PRESENT]' : '[MISSING]', | ||
allQueryParams: Object.fromEntries(clerkRequest.clerkUrl.searchParams.entries()), | ||
allCookieNames: Array.from(clerkRequest.cookies.keys()), | ||
}; | ||
|
||
logger.debug('🔍 Satellite handshake detection debug info', () => debugInfo); | ||
|
||
const hasClerkSynced = clerkRequest.clerkUrl.searchParams.has(constants.QueryParameters.ClerkSynced); | ||
const hasDevBrowser = clerkRequest.clerkUrl.searchParams.has(constants.QueryParameters.DevBrowser); | ||
const hasSessionCookie = !!clerkRequest.cookies.get(constants.Cookies.Session); | ||
const isSignedOutWithSession = requestState.status === AuthStatus.SignedOut && hasSessionCookie; | ||
|
||
const hasHandshakeCompletionIndicators = | ||
requestState.isSatellite && (hasClerkSynced || hasDevBrowser || isSignedOutWithSession); | ||
|
||
logger.debug('🔍 Handshake completion check', () => ({ | ||
isSatellite: requestState.isSatellite, | ||
hasClerkSynced, | ||
hasDevBrowser, | ||
isSignedOutWithSession, | ||
hasHandshakeCompletionIndicators, | ||
})); | ||
|
||
if (hasHandshakeCompletionIndicators) { | ||
logger.debug('🔄 Satellite handshake completion detected, re-authenticating for fresh auth state'); | ||
|
||
const beforeReAuth = { | ||
status: requestState.status, | ||
reason: requestState.reason, | ||
isSignedIn: requestState.isSignedIn, | ||
token: requestState.token ? '[PRESENT]' : '[MISSING]', | ||
}; | ||
|
||
logger.debug('📊 Auth state before re-authentication', () => beforeReAuth); | ||
|
||
// Re-authenticate to get the updated auth state after handshake completion | ||
requestState = await resolvedClerkClient.authenticateRequest( | ||
clerkRequest, | ||
createAuthenticateRequestOptions(clerkRequest, options), | ||
); | ||
|
||
const afterReAuth = { | ||
status: requestState.status, | ||
reason: requestState.reason, | ||
isSignedIn: requestState.isSignedIn, | ||
token: requestState.token ? '[PRESENT]' : '[MISSING]', | ||
}; | ||
|
||
logger.debug('✅ Auth state after re-authentication', () => afterReAuth); | ||
logger.debug('🔄 Re-auth comparison', () => ({ | ||
statusChanged: beforeReAuth.status !== afterReAuth.status, | ||
signedInChanged: beforeReAuth.isSignedIn !== afterReAuth.isSignedIn, | ||
tokenChanged: beforeReAuth.token !== afterReAuth.token, | ||
from: `${beforeReAuth.status}/${beforeReAuth.isSignedIn}`, | ||
to: `${afterReAuth.status}/${afterReAuth.isSignedIn}`, | ||
})); | ||
} | ||
|
||
const authObject = requestState.toAuth(); | ||
logger.debug('auth', () => ({ auth: authObject, debug: authObject.debug() })); | ||
|
||
const redirectToSignIn = createMiddlewareRedirectToSignIn(clerkRequest); | ||
const redirectToSignUp = createMiddlewareRedirectToSignUp(clerkRequest); | ||
if (!authObject) { | ||
throw new Error('Clerk: Auth object is null after request state resolution'); | ||
} | ||
|
||
logger.debug('🔐 Final auth object details', () => ({ | ||
userId: authObject.userId || '[NONE]', | ||
sessionId: authObject.sessionId || '[NONE]', | ||
sessionStatus: authObject.sessionStatus || '[NONE]', | ||
status: requestState.status, | ||
isSignedIn: requestState.isSignedIn, | ||
isSatellite: requestState.isSatellite, | ||
hasToken: !!requestState.token, | ||
debug: authObject.debug(), | ||
})); | ||
|
||
// Enhanced satellite domain handling: Check for unauthenticated requests on satellite domains | ||
// and ensure they can properly redirect back after authentication | ||
const isSatellite = requestState.isSatellite; | ||
const isUnauthenticated = !authObject.userId; | ||
|
||
// For satellite domains with unauthenticated users, we want to ensure proper redirect handling | ||
// This fixes the issue where public routes on satellite domains don't redirect back after auth | ||
if (isSatellite && isUnauthenticated) { | ||
logger.debug('Satellite domain unauthenticated request detected'); | ||
|
||
// If this is a satellite domain and the user is not authenticated, | ||
// we want to ensure any authentication attempts preserve the return URL | ||
const currentUrl = clerkRequest.clerkUrl.toString(); | ||
|
||
// Only apply enhanced redirect logic if we're not already on an auth page | ||
if (!currentUrl.includes('/sign-in') && !currentUrl.includes('/sign-up')) { | ||
logger.debug('Enhancing redirect functions for satellite domain', { currentUrl }); | ||
} | ||
} | ||
|
||
const redirectToSignIn = createMiddlewareRedirectToSignIn(clerkRequest, requestState); | ||
const redirectToSignUp = createMiddlewareRedirectToSignUp(clerkRequest, requestState); | ||
Comment on lines
+319
to
+320
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainVerify function signature changes across the codebase. The function signatures for 🏁 Script executed: #!/bin/bash
# Description: Search for any remaining calls to these functions that might not have been updated with the new signature.
# Search for function calls that might still use the old signature
rg -A 3 "createMiddlewareRedirectToSignIn|createMiddlewareRedirectToSignUp" --type ts --type tsx Length of output: 126 🏁 Script executed: #!/bin/bash
# Search for all calls to the redirect functions in .ts and .tsx files
rg -n "createMiddlewareRedirectToSignIn|createMiddlewareRedirectToSignUp" -g '*.ts' -g '*.tsx' Length of output: 787 Update Astro middleware to use the new redirect signatures The Astro middleware still invokes • packages/astro/src/server/clerk-middleware.ts:
🤖 Prompt for AI Agents
|
||
const protect = await createMiddlewareProtect(clerkRequest, authObject, redirectToSignIn); | ||
|
||
const authHandler = createMiddlewareAuthHandler(authObject, redirectToSignIn, redirectToSignUp); | ||
|
@@ -267,6 +381,16 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl | |
|
||
decorateRequest(clerkRequest, handlerResult, requestState, resolvedParams, keylessKeysForRequestData); | ||
|
||
// Final middleware summary | ||
logger.debug('🏁 Middleware complete', () => ({ | ||
finalAuthStatus: requestState.status, | ||
finalIsSignedIn: requestState.isSignedIn, | ||
userId: authObject.userId || '[NONE]', | ||
responseStatus: handlerResult.status, | ||
wasReAuthTriggered: hasHandshakeCompletionIndicators, | ||
isSatelliteRequest: requestState.isSatellite, | ||
})); | ||
|
||
return handlerResult; | ||
}); | ||
|
||
|
@@ -363,19 +487,41 @@ export const createAuthenticateRequestOptions = ( | |
|
||
const createMiddlewareRedirectToSignIn = ( | ||
clerkRequest: ClerkRequest, | ||
requestState: RequestState, | ||
): ClerkMiddlewareSessionAuthObject['redirectToSignIn'] => { | ||
return (opts = {}) => { | ||
const url = clerkRequest.clerkUrl.toString(); | ||
redirectToSignInError(url, opts.returnBackUrl); | ||
const returnBackUrl = opts.returnBackUrl === null ? null : (opts.returnBackUrl || url).toString(); | ||
|
||
// For satellite domains, use the satellite domain sync flow instead of standard redirects | ||
// This ensures proper cross-domain redirect handling similar to automatic satellite sync | ||
if (requestState.isSatellite && requestState.signInUrl && returnBackUrl !== null) { | ||
const redirectURL = new URL(requestState.signInUrl); | ||
redirectURL.searchParams.set('redirect_url', returnBackUrl); | ||
redirectToSignInError(redirectURL.toString(), returnBackUrl); | ||
} else { | ||
redirectToSignInError(url, returnBackUrl); | ||
} | ||
}; | ||
}; | ||
|
||
const createMiddlewareRedirectToSignUp = ( | ||
clerkRequest: ClerkRequest, | ||
requestState: RequestState, | ||
): ClerkMiddlewareSessionAuthObject['redirectToSignUp'] => { | ||
return (opts = {}) => { | ||
const url = clerkRequest.clerkUrl.toString(); | ||
redirectToSignUpError(url, opts.returnBackUrl); | ||
const returnBackUrl = opts.returnBackUrl === null ? null : (opts.returnBackUrl || url).toString(); | ||
|
||
// For satellite domains, use the satellite domain sync flow instead of standard redirects | ||
// This ensures proper cross-domain redirect handling similar to automatic satellite sync | ||
if (requestState.isSatellite && requestState.signUpUrl && returnBackUrl !== null) { | ||
const redirectURL = new URL(requestState.signUpUrl); | ||
redirectURL.searchParams.set('redirect_url', returnBackUrl); | ||
redirectToSignUpError(redirectURL.toString(), returnBackUrl); | ||
} else { | ||
redirectToSignUpError(url, returnBackUrl); | ||
} | ||
}; | ||
}; | ||
Comment on lines
488
to
526
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainSatellite domain redirect logic enhances cross-domain authentication flow. The updated redirect functions correctly implement satellite domain sync flow by adding a However, consider adding validation for the 🌐 Web query:
💡 Result: To prevent open redirect attacks in authentication flows, validating redirect URLs properly is crucial. Here are the best security practices for validating these URLs:
By implementing these best practices, you can significantly reduce the risk of open redirect vulnerabilities in your authentication flows. Add strict validation for The current satellite-domain logic appends the user-supplied • File: packages/nextjs/src/server/clerkMiddleware.ts (createMiddlewareRedirectToSignIn & createMiddlewareRedirectToSignUp, lines 384–422)
Implement these steps to ensure only legitimate URLs are used for redirection. 🤖 Prompt for AI Agents
|
||
|
||
|
Uh oh!
There was an error while loading. Please reload this page.