Skip to content

Account switching can overwrite post-login redirect and bounce users to the wrong org #112006

@dcramer

Description

@dcramer

Summary

Switching accounts across orgs in the same browser session can lose the original post-login destination and bounce the user to the wrong org/login page after a successful sign-in.

The minimal case does not require two concurrent login flows. A user can:

  1. sign in to sentry.io as account A and use org A
  2. later open an org/page that requires account B
  3. get sent to sign in
  4. sign in as account B
  5. get bounced to the wrong org/login page instead of the originally requested org/page

The core bug appears to be:

  • the auth-required path stores the destination in session _next
  • the org-login POST path falls back to that session value unless ?next= is present in the request
  • switching from authenticated user A to authenticated user B causes Django auth to flush() the session
  • _next is therefore lost on the account switch itself

Once _next is gone, the flow falls back to generic login/implicit org resolution, which can send the user somewhere other than the org/page that originally triggered re-auth.

User Impact

  • Account switching feels unreliable even in a single-tab, sequential flow.
  • A successful login looks like it failed or looped.
  • Users get sent back to the wrong org's login page and see a "sign in with a different account" style warning.

This is separate from the expected "one browser profile has one active Sentry account session" behavior. Re-auth is expected. Redirecting to the wrong org after re-auth is the bug.

Reproduction

  1. Sign in to sentry.io as account A.
  2. Use org A normally.
  3. Later, in the same browser session, open an org/page that account A cannot access but account B can.
  4. Sentry presents sign-in.
  5. Sign in as account B.

Expected

After signing in as account B, the user should return to the org/page that originally triggered re-auth.

Actual

After signing in as account B, Sentry can bounce the user to org A's login/warning page instead. Retrying from the intended org can then succeed, which makes the original sign-in look flaky.

Code Path

  • Org views deliberately treat "org exists but the current session cannot access it" as auth-required so the user can re-authenticate as a different account:
    • src/sentry/web/frontend/base.py:615
    • src/sentry/web/frontend/base.py:631
  • React org pages defer to the normal auth-required handler:
    • src/sentry/web/frontend/react_page.py:183
  • The standard auth-required handler stores the requested destination in session _next and redirects to login:
    • src/sentry/web/frontend/base.py:495
    • src/sentry/web/decorators.py:21
  • _next is global mutable session state and is cleared/re-written whenever a new login flow is initiated:
    • src/sentry/utils/auth.py:144
  • The org-specific login page already has a partial fix for multi-tab flows by re-reading next on POST:
    • src/sentry/web/frontend/auth_organization_login.py:71
  • But that POST path only helps if next is present in the request. The standard auth-required redirect usually does not append ?next=... to the login URL:
    • src/sentry/web/frontend/base.py:495
  • On account switch, Django auth flushes the existing session when the current authenticated user changes:
    • .venv/lib/python*/site-packages/django/contrib/auth/__init__.py:153
  • Sentry's auth wrapper calls Django login during the account switch:
    • src/sentry/utils/auth.py:294
    • src/sentry/utils/auth.py:362
  • That means _next written earlier in the flow is dropped when user A becomes user B, unless next was carried in the request itself.
  • After login, the flow resolves the final destination from get_login_redirect(), which consumes session _next:
    • src/sentry/web/frontend/auth_login.py:460
    • src/sentry/api/endpoints/auth_login.py:78
    • src/sentry/utils/auth.py:174
  • Immediately after login, _handle_login() also recomputes active_organization without passing an explicit org slug:
    • src/sentry/web/frontend/auth_login.py:480
  • determine_active_organization() falls back to implicit org state when no explicit org slug is provided:
    • src/sentry/web/frontend/base.py:163
    • src/sentry/web/frontend/base.py:225

Where Things Go Wrong

The standard auth-required redirect relies on session _next to remember where the user was trying to go.

That is already fragile, but the more specific bug is that account switching destroys that state:

  1. user A hits org/page for B
  2. Sentry stores the original destination in session _next
  3. org login POST reuses _next if ?next= was not present in the URL
  4. successful login as user B calls Django login()
  5. Django sees the session belongs to a different authenticated user and flushes the session
  6. _next is gone
  7. final redirect falls back to generic login/default org resolution instead of the original requested destination

So the original destination is not just "sometimes wrong"; it is structurally lost on account switch when the login flow depends on session _next instead of request-scoped next.

The remaining variability is in the fallback destination after _next is lost. That fallback depends on host/subdomain and implicit org selection, which is why the user can end up on the wrong org/login page.

Fix Direction

  1. Always append next when redirecting to login, especially in the standard auth-required path.
  2. On login POST, prefer a request-scoped next from GET or POST over session fallback.
  3. Avoid recomputing active org from implicit session activeorg immediately after an account switch unless the requested org is explicitly absent.
  4. Longer-term, stop relying on a single global _next slot for login continuation and move to a per-flow state token.

Metadata

Metadata

Assignees

No one assigned

    Labels

    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