getsentry / getsentry/sentry

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

Open
#112,006 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
44.8k
Forks
4.9k
Avg merge
21h 10m
Merged PRs (30d)
635

Description

## 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.

Contributor guide

Open the contributing guide

Research direction

Start at src/sentry/web/frontend/base.py:495 and src/sentry/web/frontend/auth_organization_login.py:71, then trace the login flow through src/sentry/utils/auth.py and auth_login.py. Preserve the originally requested destination through an account switch and verify that login returns the user to the intended org or page rather than implicit fallback resolution.

Written by the indexing model from the issue text.

Assessment

Tech stack
django, python
Domain
authentication, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.