fix(frontend): preserve redirect target through SSO login (#13270)

The redirect target set by ProtectedRoute travels in react-router state,
which does not survive the full page navigation through the identity
provider. After the SSO round trip users always landed on /home.

Forward the target through the flow as a 'next' query param on the
allauth callback URL instead, and recover it on the /logged-in page.
Falls back to /home when no valid in-app path is present.

Co-authored-by: Matthias Mair <code@mjmair.com>
Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-11-14 13:11:56 +11:00
1 parent 5b5daa5b86
commit b9b46797c2
3 files changed
+33 -8

No files matched your search

@@ -16,6 +16,7 @@ import {
import type { AuthProvider } from '@lib/types/Auth';
import { t } from '@lingui/core/macro';
import { useLocation } from 'react-router-dom';
import { ProviderLogin } from '../../functions/auth';
import type { JSX } from 'react';
@@ -39,6 +40,8 @@ function getBrandIcon(provider: AuthProvider) {
}
export function SsoButton({ provider }: Readonly<{ provider: AuthProvider }>) {
const location = useLocation();
return (
<Tooltip
label={t`You will be redirected to the provider for further actions.`}
@@ -46,7 +49,7 @@ export function SsoButton({ provider }: Readonly<{ provider: AuthProvider }>) {
<Button
leftSection={getBrandIcon(provider)}
radius='xl'
onClick={() => ProviderLogin(provider)}
onClick={() => void ProviderLogin(provider, 'login', location?.state)}
>
{provider.name}
</Button>
+16 -5
View File
@@ -558,9 +558,23 @@ export function clearCsrfCookie() {
export async function ProviderLogin(
provider: AuthProvider,
process: 'login' | 'connect' = 'login'
process: 'login' | 'connect' = 'login',
redirect?: any
) {
await ensureCsrf();
// The round-trip through the identity provider is a full page navigation,
// so the router state carrying the originally requested URL is lost on
// the way back. Forward it through the flow instead, appended to the
// callback URL allauth redirects the browser back to - LoggedIn.tsx
// picks the 'next' parameter back up once the login completes.
const next = `${redirect?.redirectUrl ?? ''}${redirect?.queryParams ?? ''}`;
const callback_url =
generateUrl(`/${getBaseUrl()}/logged-in`, window.location.origin) +
(process === 'login' && next.startsWith('/') && !next.startsWith('//')
? `?next=${encodeURIComponent(next)}`
: '');
post(generateUrl(apiUrl(ApiEndpoints.auth_provider_redirect)), {
provider: provider.id,
// Return to wherever this page is actually being served from, not the
@@ -570,10 +584,7 @@ export async function ProviderLogin(
// back in server-side by Django's compatibility redirect for bare
// (non-API-host) paths, which only exists on the Django side - so add
// it explicitly rather than relying on that.
callback_url: generateUrl(
`/${getBaseUrl()}/logged-in`,
window.location.origin
),
callback_url,
process: process,
csrfmiddlewaretoken: getCsrfCookie()
});
+13 -2
View File
@@ -27,6 +27,15 @@ function ssoErrorMessage(error: string): string {
}
}
// Recovers the redirect target from the 'next' query param ProviderLogin
// appends to the SSO callback URL - router state does not survive the full
// page load back from the identity provider.
function redirectFromNextParam(next: string | null) {
return next?.startsWith('/') && !next.startsWith('//')
? { redirectUrl: next }
: undefined;
}
export default function Logged_In() {
const navigate = useNavigate();
const location = useLocation();
@@ -34,6 +43,8 @@ export default function Logged_In() {
useEffect(() => {
const error = searchParams.get('error');
const redirect =
location?.state ?? redirectFromNextParam(searchParams.get('next'));
if (error) {
showLoginNotification({
@@ -41,11 +52,11 @@ export default function Logged_In() {
message: ssoErrorMessage(error),
success: false
});
navigate('/login', { state: location?.state });
navigate('/login', { state: redirect });
return;
}
checkLoginState(navigate, location?.state);
void checkLoginState(navigate, redirect);
}, [navigate]);
return (