# Account state not enforced on alternate auth paths

Does every login, token, and API path re-check disabled or expired account state, not just the primary one?

_Area: Web authentication and account takeover. Seen in 3 disclosed reports; latest 2026-07-21. Updated 2026-11-02._

## How it works
An account carries a state: active, disabled, deactivated, password expired. The application enforces that state on one entry point (often the web login or REST API) but a second entry point that mints a session or accepts a token skips the check. The broken assumption is that "this account is valid" is established once and holds everywhere. In practice each entry point authorizes independently, and any entry point that checks only token scope, signature, or external identity lets a disabled or expired account through. Sometimes the alternate path does worse than skip the check: it actively resets the state flag to active [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-g9g6-qhrc-p3qc.md).

## Where it shows up
- Multiple auth surfaces: REST, GraphQL, Git-over-HTTP, OAuth2 callback, websocket — each with its own authorization code [E1](https://bugbountylibrary.com/r/h1%3A1285226.md)[E3](https://bugbountylibrary.com/r/h1%3A1192460.md).
- A login handler that sets an `IsActive`/`enabled`/`status` field from the external identity instead of rejecting [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-g9g6-qhrc-p3qc.md).
- Policy objects that deny a coarse permission (`:access_api`) while a controller authorizes on token scope alone [E3](https://bugbountylibrary.com/r/h1%3A1192460.md).
- Token/JWT gateways that trust residual claims (user id, org, seat) without a server-side lookup of current account state [E1](https://bugbountylibrary.com/r/h1%3A1285226.md).
- Recent merge requests touching one auth path (e.g. LDAP) that may have altered shared enforcement [E1](https://bugbountylibrary.com/r/h1%3A1285226.md).

## How to review
The invariant: **every path that mints a session or accepts a credential must independently evaluate current account state (active, not disabled, password not expired) before granting access.** Check each entry point and confirm it calls the same state check as the primary login. For OAuth/SSO callbacks, confirm a locally-disabled account is rejected, not reconciled to active [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-g9g6-qhrc-p3qc.md). For token/JWT gateways, confirm the gateway or downstream app re-validates account state server-side rather than trusting residual claims [E1](https://bugbountylibrary.com/r/h1%3A1285226.md). For alternate APIs, confirm the controller applies the deactivated/expired policy, not just scope [E3](https://bugbountylibrary.com/r/h1%3A1192460.md).

## How finders spotted it
- Read the changelog, saw a global policy denying deactivated users `:access_api`, hypothesized GraphQL only checks `api` scope, and confirmed REST returned 403 while GraphQL succeeded [E3](https://bugbountylibrary.com/r/h1%3A1192460.md).
- Tracked prior patches that blocked expired-password API access, noticed a later LDAP-fix merge request, and tested REST/GraphQL/Git with an expired-password token [E1](https://bugbountylibrary.com/r/h1%3A1285226.md).
- Read the OAuth callback code, saw it flip `IsActive` while the local-credential path rendered an inactive-user error, and noted the flag was only checked on later requests [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-g9g6-qhrc-p3qc.md).

## Variants seen
- Deactivated user reaches GraphQL because only token scope is checked [E3](https://bugbountylibrary.com/r/h1%3A1192460.md).
- Expired-password user reaches REST, GraphQL, and Git via tokens after a regression [E1](https://bugbountylibrary.com/r/h1%3A1285226.md).
- OAuth2 callback silently re-enables an admin-disabled account [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-g9g6-qhrc-p3qc.md).

## Why fixes fail
A fix for one path can regress another: the LDAP expired-password fix re-opened API/Git access [E1](https://bugbountylibrary.com/r/h1%3A1285226.md). Enforcement added at login does not cover token-minting paths [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-g9g6-qhrc-p3qc.md)[E3](https://bugbountylibrary.com/r/h1%3A1192460.md).

## Impact and severity
Full account access despite administrative disable or expiry: data read/write via GraphQL [E3](https://bugbountylibrary.com/r/h1%3A1192460.md), API and Git access with expired password [E1](https://bugbountylibrary.com/r/h1%3A1285226.md), and silent defeat of the incident-response disable toggle in SSO deployments [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-g9g6-qhrc-p3qc.md). Impact rises when the bypass reaches many surfaces [E1](https://bugbountylibrary.com/r/h1%3A1285226.md) or undoes an admin control meant to contain a compromised account [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-g9g6-qhrc-p3qc.md).

## Evidence
- E1: https://bugbountylibrary.com/r/h1%3A1285226.md
- E2: https://bugbountylibrary.com/r/ghsa%3AGHSA-g9g6-qhrc-p3qc.md
- E3: https://bugbountylibrary.com/r/h1%3A1192460.md
