When I talk about auth with other developers, everyone asks about the login flow. Nobody asks about the logout loop. But the loose ends are where auth bugs live: the token that stays in localStorage after "logout", the role a user assigns to themselves during registration, the expired token that leaves you staring at a broken dashboard with no idea what happened.
I run a small Laravel + React starter kit I sell to indie hackers, so I've had to make these decisions concretely, not theoretically. Here is the full logout loop as I ship it — backend, frontend, and the boring-but-deliberate choices in between.
The backend: logout revokes the token, not the account
With Laravel Sanctum, the logout endpoint does exactly one thing:
public function logout(Request $request): JsonResponse
{
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Logged out']);
}
Note what this does not do: it does not delete all of the user's tokens. It revokes the token used for this request. That is a deliberate scoping decision — logging out on your laptop does not boot you from your phone. It also means "logout everywhere" would need a separate endpoint ($request->user()->tokens()->delete()), which I have not shipped because the kit has no settings page that needs it yet. Document the boundary you chose; do not pretend it does not exist.
Two more backend decisions in the same spirit:
Registration never trusts the client with the role. The register handler hardcodes it:
$user = User::create([
'name' => $request->validated('name'),
'email' => $request->validated('email'),
'password' => $request->validated('password'),
'role' => 'user', // always. never from the request.
]);
A role field on the request would be a privilege-escalation form with extra steps. There is no password reset or email verification in the kit either — the surface stays small on purpose.
Login failures are boring on purpose. Wrong credentials get a 401 with {"message": "Invalid credentials"}. No "email not found" vs "wrong password" distinction. The frontend can render it blind, and it leaks nothing to an attacker enumerating accounts.
Admin routes sit behind both guards — auth:sanctum and a role:admin middleware (EnsureUserHasRole) — so even a valid token with the wrong role gets nothing.
The frontend: one client, one cleanup rule
The React side has exactly one axios client (src/api/client.js), and auth behaviour lives in its interceptors — not scattered across components:
-
Request interceptor attaches the Bearer token from
localStorage(saas_token) on every call. One place, so it is impossible for a page to "forget" auth. -
Response interceptor watches for 401. On any 401, it clears the stored token and redirects to
/login— unless the user is already on/loginor/register. That exception matters: without it, a 401 on the login page itself would trigger a redirect to the page you are already on, or worse, a loop. Public auth pages cannot be "protected" by the same rule.
Then AuthContext closes the remaining gap: the re-verification on load. When the app mounts, if a token exists in storage, it calls GET /auth/me:
- Success: the session is real, set the user.
- Failure: clear the token, clear the user, treat the visitor as logged out.
A token in localStorage is a claim, not proof. The /me call turns the claim into a fact on every page load, and an expired or revoked token silently becomes a logged-out state instead of a broken dashboard.
Finally, the logout function itself is best-effort by design:
const logout = useCallback(async () => {
// Best-effort: the session is cleared locally no matter what.
try {
await client.post('/auth/logout');
} catch {
// Ignore — token is being discarded either way.
}
localStorage.removeItem('saas_token');
setToken(null);
setUser(null);
}, []);
The server call revokes the token when the network cooperates. The local cleanup happens unconditionally — a failed network request must never leave a "logged out" user with a live token sitting in storage and a dashboard that still thinks they are signed in.
The lesson I keep re-learning
Decide what "logged out" means everywhere at once: server token gone, local token gone, redirect to login. If those three are handled in three different places with three different assumptions, you will have an auth bug — it is only a question of when.
And test the loop, not just the endpoints. My mental checklist before shipping: log in, refresh (does /me re-verify?), let the token die, hit a protected page (do I land on login without a loop?), log out on flaky network (is the local state still clean?). Boring. Deliberate. That is the point.
This is the auth loop I ship in my Laravel + React SaaS Starter Kit ($19 one-time) — Sanctum auth, role-based access, one axios client with the interceptors above, and a dashboard that stays boring in all the right ways.
Top comments (0)