Skip to content

Redirect after SSO login ends on last_page instead of intended URL #27426

@sekaninovam

Description

@sekaninovam

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:

  1. Open a fresh browser session (no active Directus session)
  2. Navigate directly to a protected page, e.g. http://localhost:8055/admin/settings/data-model
  3. Directus redirects to /admin/login?redirect=%2Fsettings%2Fdata-model
  4. Click the SSO login button
  5. Complete authentication with the SSO provider
  6. 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:

  1. Page loads at /login?redirect=/settings/data-model&continue=
  2. login.vue renders ContinueAs (user is authenticated)
  3. onMounted detects continue in query → calls hydrateAndLogin() (first call)
  4. Inside hydrateAndLogin(), await hydrate() is called
  5. hydrate() calls onHydrateModules() → router.addRoute() for each module
  6. Vue Router detects newly registered routes and triggers a re-navigation back to the current URL (/login?redirect=...&continue=)
  7. ContinueAs is re-mounted → onMounted fires again → hydrateAndLogin() (second call)
  8. Meanwhile, the first call completes: reads query.redirect = "/settings/data-model" → router.push("/settings/data-model") ✓
  9. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions