adjustfocus

TFOL Dev Story #2

January 06, 2026

How a single leftover session variable broke login, navigation, and reality

There are bugs that fail loudly.
There are bugs that fail silently.
And then there are bugs that only appear after everything seems to be working.

This is the story of one of those.


The Symptom (or so I thought)

Everything worked:

  • Guests could browse websites

  • Members could log in

  • Navigation menus rendered correctly

  • Website switching worked — mostly

But then a strange pattern emerged:

If I logged in, switched websites, logged out, and tried to log in again — the login form wouldn’t appear.
It just bounced back to the home page.

Worse:

  • Restarting the browser fixed it

  • Clearing cookies fixed it

  • Fresh sessions worked perfectly

Classic “it must be caching something” vibes.

It wasn’t.


The Pattern That Gave It Away

The bug only appeared after this sequence:

  1. Start as Guest

  2. Log in as a member

  3. Switch websites

  4. Log out (back to Guest)

  5. Click “Log in” again

At step 5, the login page refused to render.

That told me something important:

Some piece of “logged-in identity” was surviving logout.

The Wrong Assumption

Like many PHP apps, TFOL treated this as “logged in”:

$_SESSION['id'] > 0
$_SESSION['account_id'] > 0

And on logout or website switch, those values were being cleared.

So naturally I assumed the session was clean.

It wasn’t.


The Real Culprit: Identity Metadata Leak

After adding temporary logging inside login.php, the truth became obvious.

Even after logout, the session still contained:

{
  "id": 0,
  "account_id": 0,
  "role": "admin",
  "forename": "Geoffrey"
}

So although the user was technically logged out…

  • The role still said admin

  • The name still said Geoffrey

That single leftover metadata caused:

  • Navigation to think a member existed

  • Links like /member/0 to be generated

  • Login logic to redirect instead of render

The app was half-logged-in.


Why It Was So Hard to Spot

This bug had three properties that made it deceptive:

  1. No errors
    PHP didn’t complain. Routing didn’t break.

  2. State-dependent
    Fresh sessions worked perfectly.

  3. Order-dependent
    Only happened after login → switch → logout.

This wasn’t a syntax bug.
It was a session lifecycle bug.


The Fix (The Right One)

The solution was not more conditionals.
It was centralizing what “Guest” actually means.

When forcing Guest mode, clear all identity state:

$_SESSION['id'] = 0;
$_SESSION['account_id'] = 0;

unset(
    $_SESSION['role'],
    $_SESSION['forename'],
    $_SESSION['surname'],
    $_SESSION['email'],
    $_SESSION['member'],
    $_SESSION['menu_owner_id'],
    $_SESSION['section']
);

$_SESSION['role'] = 'guest';
session_regenerate_id(true);

And then apply this everywhere it matters:

  • Website switching

  • Logout

  • Any forced guest transition


Hardening the UI (Defense in Depth)

Navigation was updated to treat users as logged in only if both are true:

{% set is_logged_in =
    session.id|default(0) > 0
    and session.account_id|default(0) > 0
%}

So even if metadata ever leaks again, it won’t generate invalid member routes.


The Lesson

Session bugs are not about authentication.
They’re about identity boundaries.

Logging out isn’t just “unset the ID”.

It’s:

  • Clearing identity

  • Clearing role

  • Clearing cached objects

  • Resetting expectations

If any one of those survives, reality bends.


Final Takeaway

If a bug:

  • Only appears after logout

  • Disappears on browser restart

  • Refuses to reproduce in clean sessions

Then it’s almost always this:

Something in the session still thinks you’re someone else.

And the fix isn’t clever logic —
it’s ruthless cleanup.


 

Posted in tfol-dev-stories by TFOL BLOG

Comments