Describe the Bug
After a successful SSO (OpenID) login where the original URL contained both redirect and continue query parameters (e.g. /login?redirect=/settings/data-model&continue=), the user is redirected to their last_page from the database instead of the originally requested page.
The root cause is a race condition in continue-as.vue: hydrateAndLogin() is invoked twice due to Vue Router re-navigating back to /login?redirect=...&continue= after router.addRoute() is called inside hydrate(). The second invocation reads query.redirect from an already-changed currentRoute (which no longer contains the redirect param), falls through to lastPage.value, and navigates there instead.
This affects any SSO flow where continue is present in the URL — which is always the case, since sso-links.vue unconditionally appends &continue= to the redirect URL.
The bug does not occur when the user manually clicks the Continue button (URL without &continue=), because in that case hydrateAndLogin() is only called once.
Relation to Previous Issues and PRs
This bug is related to but distinct from issue #19421 ("last_page is ignored if user logs in via OAuth"), which was fixed in PR #25049 (merged in v11.7.0).
PR #25049 added await userPromise to ensure lastPage.value is populated before navigation. However, this change extended the duration of hydrateAndLogin(), which increased the likelihood of the Vue Router re-navigation (step 6 in reproduce root cause analysis steps) completing and remounting ContinueAs before the first call finishes. The fix in #25049 resolved the original issue but made the conditions for this race condition more reliable to hit.
To Reproduce
Prerequisites:
- Directus with an OpenID SSO provider configured (tested with Microsoft Entra)
- User has a previously stored last_page value (any page other than the target)
Steps:
- Open a fresh browser session (no active Directus session)
- Navigate directly to a protected page, e.g. http://localhost:8055/admin/settings/data-model
- Directus redirects to /admin/login?redirect=%2Fsettings%2Fdata-model
- Click the SSO login button
- Complete authentication with the SSO provider
- SSO callback redirects back to /admin/login?redirect=%2Fsettings%2Fdata-model&continue=
Expected result: User lands on /settings/data-model
Actual result: User briefly appears on /settings/data-model then is immediately redirected to their last_page (e.g. /file-view/securities/XY)
Additional reproduction case (no fresh session required):
While already logged in, navigate directly to:
http://localhost:8055/admin/login?redirect=/settings/data-model&continue=
Result: ends up on last_page instead of /settings/data-model.
Compare with (same but without &continue=):
http://localhost:8055/admin/login?redirect=/settings/data-model
Result: Continue button appears, clicking it correctly navigates to /settings/data-model.
Root Cause Analysis
The sequence of events that causes the double invocation:
- Page loads at /login?redirect=/settings/data-model&continue=
- login.vue renders ContinueAs (user is authenticated)
- onMounted detects continue in query → calls hydrateAndLogin() (first call)
- Inside hydrateAndLogin(), await hydrate() is called
- hydrate() calls onHydrateModules() → router.addRoute() for each module
- Vue Router detects newly registered routes and triggers a re-navigation back to the current URL (/login?redirect=...&continue=)
- ContinueAs is re-mounted → onMounted fires again → hydrateAndLogin() (second call)
- Meanwhile, the first call completes: reads query.redirect = "/settings/data-model" → router.push("/settings/data-model") ✓
- The second call then runs: currentRoute is now /settings/data-model (no redirect param) → query.redirect is undefined → falls back to lastPage.value → router.push(lastPage) ✗
This was confirmed via history.replaceState/pushState instrumentation showing the navigation sequence:
replace /login?redirect=/settings/data-model&continue= ← guard redirect after hydrate
replace /login?redirect=/settings/data-model&continue= ← finalization
push /settings/data-model ← first hydrateAndLogin ✓
push /file-view/securities/XY ← second hydrateAndLogin ✗
Directus Version
v11.17.4
Hosting Strategy
Self-Hosted (Docker Image)
Database
PostgreSQL 18
Describe the Bug
After a successful SSO (OpenID) login where the original URL contained both redirect and continue query parameters (e.g. /login?redirect=/settings/data-model&continue=), the user is redirected to their last_page from the database instead of the originally requested page.
The root cause is a race condition in continue-as.vue: hydrateAndLogin() is invoked twice due to Vue Router re-navigating back to /login?redirect=...&continue= after router.addRoute() is called inside hydrate(). The second invocation reads query.redirect from an already-changed currentRoute (which no longer contains the redirect param), falls through to lastPage.value, and navigates there instead.
This affects any SSO flow where continue is present in the URL — which is always the case, since sso-links.vue unconditionally appends &continue= to the redirect URL.
The bug does not occur when the user manually clicks the Continue button (URL without &continue=), because in that case hydrateAndLogin() is only called once.
Relation to Previous Issues and PRs
This bug is related to but distinct from issue #19421 ("last_page is ignored if user logs in via OAuth"), which was fixed in PR #25049 (merged in v11.7.0).
PR #25049 added await userPromise to ensure lastPage.value is populated before navigation. However, this change extended the duration of hydrateAndLogin(), which increased the likelihood of the Vue Router re-navigation (step 6 in reproduce root cause analysis steps) completing and remounting ContinueAs before the first call finishes. The fix in #25049 resolved the original issue but made the conditions for this race condition more reliable to hit.
To Reproduce
Prerequisites:
Steps:
Expected result: User lands on /settings/data-model
Actual result: User briefly appears on /settings/data-model then is immediately redirected to their last_page (e.g. /file-view/securities/XY)
Additional reproduction case (no fresh session required):
While already logged in, navigate directly to:
http://localhost:8055/admin/login?redirect=/settings/data-model&continue=
Result: ends up on last_page instead of /settings/data-model.
Compare with (same but without &continue=):
http://localhost:8055/admin/login?redirect=/settings/data-model
Result: Continue button appears, clicking it correctly navigates to /settings/data-model.
Root Cause Analysis
The sequence of events that causes the double invocation:
This was confirmed via history.replaceState/pushState instrumentation showing the navigation sequence:
replace /login?redirect=/settings/data-model&continue= ← guard redirect after hydrate
replace /login?redirect=/settings/data-model&continue= ← finalization
push /settings/data-model ← first hydrateAndLogin ✓
push /file-view/securities/XY ← second hydrateAndLogin ✗
Directus Version
v11.17.4
Hosting Strategy
Self-Hosted (Docker Image)
Database
PostgreSQL 18