diff --git a/src/frontend/src/components/buttons/SSOButton.tsx b/src/frontend/src/components/buttons/SSOButton.tsx index aaa7c983c5..e8581fbc74 100644 --- a/src/frontend/src/components/buttons/SSOButton.tsx +++ b/src/frontend/src/components/buttons/SSOButton.tsx @@ -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 ( ) { diff --git a/src/frontend/src/functions/auth.tsx b/src/frontend/src/functions/auth.tsx index 6a5a9f382a..034861c28d 100644 --- a/src/frontend/src/functions/auth.tsx +++ b/src/frontend/src/functions/auth.tsx @@ -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() }); diff --git a/src/frontend/src/pages/Auth/LoggedIn.tsx b/src/frontend/src/pages/Auth/LoggedIn.tsx index 8bae4d78ae..c01f9b6d2a 100644 --- a/src/frontend/src/pages/Auth/LoggedIn.tsx +++ b/src/frontend/src/pages/Auth/LoggedIn.tsx @@ -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 (