The screen is evidence. The server decides whether the session is alive.
At 09:14, a support agent resets their password after seeing an unfamiliar sign-in alert. At 09:22, they report that a workstation browser still shows the account dashboard. The report does not yet tell us whether that browser made a new request, reused an old session, or is showing data already loaded in the page.
Write down the exact account, browser, route, timestamps, and action used to check access. Avoid collecting the session token itself in a ticket or log; it is a credential. Use a disposable account and sessions when reproducing the behavior.
- Asset
- The employee’s account and the data reachable through it.
- Actor
- The account holder, plus anyone who may hold an issued session token.
- Observed
- A dashboard remained visible after the password reset.
- Invariant
- After a completed reset, a session issued before that reset cannot authenticate a new request.
A recovery token proves control of a recovery channel under the flow’s assumptions.
The credential verifier changes for future sign-ins.
A separate bearer credential may still be presented on each request.
The server accepts or revokes that credential.
Authentication establishes which account a sign-in represents. A session maintains continuity after that authentication. Authorization is a separate decision about whether this account may perform a particular action on a particular resource. A valid session does not mean “may do anything.”
Several causes can produce the same open dashboard.
Before choosing “revoke sessions,” list explanations that fit the report. The page may be cached; the check may have happened before reset completion; the workstation may have signed in again; or the server may still accept a session created earlier. Each predicts a different result when we make a fresh request with a controlled old token.
For a safe reproduction, create two sessions for a test account, record only which test fixture owns each token (never print the token), complete the reset, and make a fresh request from each browser context. Inspect the reset completion timestamp and server-side session lookup result. Keep the identity and route fixed so only session age changes.
Compare your diagnosis with the fixtureReveal after predicting the check
- Observation
- The workstation’s new request receives authenticated account data after the reset response completed.
- Competing causes
- A fresh sign-in occurred; the request used a pre-reset session still accepted by the server; or the reset was applied to another account.
- Discriminating check
- Use a test account, preserve one pre-reset session, verify the account ID and reset completion, then make a new server request with that old session. In parallel, verify a successful fresh sign-in.
- Conclusion if reproduced
- If the old token alone is accepted after reset and no new sign-in occurred, the session lifecycle did not meet the stated invariant. This does not prove the reported employee’s token was stolen.
A password and a session token have different jobs.
A password is presented to establish a sign-in. Afterward, many web applications issue a random session identifier in a cookie. The browser presents that identifier on later requests; the server maps it to an account and session state. Whoever possesses a usable bearer token may be able to act through that session until the server rejects it.
Changing the password updates the verifier used on the next sign-in. It does not, by itself, change the bytes in another browser’s cookie or delete a server-side session record. So ask where session validity lives: a database row, cache, version check, identity provider, signed token, or some combination. The reset must reach that authority if old sessions are meant to stop working immediately.
An accepted old session carries the authority of that session.
If a pre-reset token remains valid, whoever can present it may reach routes that accept the associated account session. Depending on the account’s permissions and each route’s authorization checks, that can expose private records, change account settings, or initiate business actions. A reset that fails to revoke a session can therefore leave a real path for continued access after the account holder believes recovery is complete.
The report alone does not show that an attacker had the token, which data was accessed, or whether any action was taken. Check session issuance and revocation events, account changes, and relevant access records while preserving their integrity. Avoid putting raw session identifiers in diagnostic logs; use a keyed digest or internal session record ID if correlation is needed.
- Can establish
- A server accepted a session created before a completed reset, if the controlled request reproduces it.
- May enable
- Actions allowed to that account by reachable routes and their authorization checks.
- Does not establish
- Token theft, attacker identity, data exfiltration, or that reset itself was bypassed.
- Still investigate
- Session history and sensitive actions in the relevant window, without treating a missing log as proof that nothing happened.
Change the authenticator and retire sessions as one transition.
For server-side sessions, a direct strategy is to update the password hash and delete that user’s sessions in the same database transaction. If the operation fails halfway, rollback must not leave a new password with old sessions still accepted. The examples below sketch that persistence boundary; they are not a complete password-reset endpoint.
The examples use application-owned transaction and session-store interfaces.
// The store methods below are application-owned persistence operations.
// Keep the credential update and session revocation in one transaction.
export async function resetPassword(
userId: string,
newPasswordHash: string,
db: Database
): Promise<void> {
await db.transaction(async (tx) => {
await tx.users.replacePasswordHash(userId, newPasswordHash);
await tx.sessions.deleteAllForUser(userId);
});
}
export async function resolveSession(
token: string,
store: SessionStore
): Promise<AuthenticatedUser | null> {
const session = await store.findByToken(token);
if (!session || session.expiresAt <= Date.now()) return null;
return store.findUser(session.userId);
}
package sessions
import "context"
// Store methods are application-owned persistence operations.
// Keep the credential update and session revocation in one transaction.
func ResetPassword(ctx context.Context, db Database, userID, newPasswordHash string) error {
return db.Transaction(ctx, func(tx Tx) error {
if err := tx.ReplacePasswordHash(ctx, userID, newPasswordHash); err != nil {
return err
}
return tx.DeleteAllSessionsForUser(ctx, userID)
})
}
func ResolveSession(ctx context.Context, store SessionStore, token string) (*User, error) {
session, err := store.FindByToken(ctx, token)
if err != nil {
return nil, err
}
if session == nil || !session.ExpiresAt.After(store.Now()) {
return nil, ErrUnauthenticated
}
return store.FindUser(ctx, session.UserID)
}
The request validator must consult the same authority: a deleted session cannot resolve to an authenticated user. Clearing a cookie in the browser improves local cleanup, but it does not revoke a copy held elsewhere. If sessions live in a cache or identity provider, use that system’s revocation operation and define what happens when it is unavailable.
For signed, self-contained tokens, deleting a database row that the validator never reads changes nothing. A credential-version check, revocation list, short token lifetime with an explicit residual-risk decision, or a provider-supported revocation flow can make the reset visible to those validators. Each adds a consistency or availability tradeoff; document the maximum time an old token can remain usable.
A reset test must protect recovery and ordinary sign-in.
Write the expected behavior before wiring up a control. Tests should exercise the real session store or validator, not merely assert that a delete method was called. Keep account identity and request route constant while varying whether the presented session predates the reset.
An ordinary authenticated request succeeds before the lifecycle transition.
A fresh request with the pre-reset token is unauthenticated.
The new password can establish a fresh session that resolves normally.
A failed transaction does not silently split password and session state.
Test cases to keepReview the contract before implementing the test
- Issue two sessions, reset the password, then prove neither old session authenticates a new request.
- Sign in with the new password and prove the newly issued session works.
- Verify the previous password no longer signs in, while account recovery and normal sign-in still return the expected safe responses.
- Inject a persistence failure and assert the transaction leaves password and session state consistent.
- If revocation propagates asynchronously, measure and assert the documented propagation bound at every validator.
Choose the control that matches the evidence and session design.
A customer’s old browser still loads a private billing page after reset. What do you do next?
You have confirmed a fresh server request, the correct account, a completed reset, and a pre-reset test session. Which next change best preserves the contract?
The useful habit is not “always log out everywhere.” It is to name the credential whose meaning changed, find the authority that accepts it, and verify the lifecycle transition at that authority. Apply the same investigation to password changes, MFA removal, role changes, account recovery, and logout.
For deeper reference, read OWASP’s Forgot Password Cheat Sheet and Session Management Cheat Sheet. Checked 2026-09-30 for guidance on existing-session invalidation and server-side session lifecycle; this lesson does not assess any specific product’s implementation.